Golang gRPC单流/服务端流选型与数据转换冗余优化问询
问题描述
当前使用gRPC一元API的Golang服务端中,GetUsers接口返回的*[]User类型和protobuf定义的*pb.GetUsersResponse不匹配,需要循环做类型转换;客户端又得再次循环把gRPC返回结果转成JSON格式返回给前端。想搞清楚两个问题:
- 能不能避免这种双重循环?
- 应该选gRPC一元API还是服务端流?看起来服务端流能把循环次数减到一次。
附:当前代码实现
服务端gRPC代码
func (s *usersGRPCServer) GetUsers(ctx context.Context, empty *empty.Empty) (*pb.GetUsersResponse, error) { users, err := s.service.GetUsers() if err != nil { log.Println(err) return nil, status.Error(codes.Internal, "Internal Server Error") } // Convert []*User to []*pb.User directly var pbUsers []*pb.User for _, user := range *users { pbUsers = append(pbUsers, &pb.User{ FirstName: user.FirstName, LastName: user.LastName, Username: user.Username, Email: user.Email, Active: int64(user.Active), Admin: int64(user.Admin), }) } return &pb.GetUsersResponse{ Users: pbUsers, }, nil }
客户端gRPC调用代码
// client grpc func (app *application) grpcGetUsersHandler(urlString string) gin.HandlerFunc { return func(c *gin.Context) { grpcClient, _, err := newUsersGRPCClient(urlString) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status": http.StatusInternalServerError, "message": "Internal Server Error"}) } ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() resp, err := grpcClient.GetUsers(ctx, &empty.Empty{}) if err != nil { log.Println(err) c.JSON(http.StatusInternalServerError, gin.H{"status": http.StatusInternalServerError, "message": "Internal Server Error"}) return } var usersData []map[string]interface{} for _, user := range resp.Users { userData := map[string]interface{}{ "first_name": user.FirstName, "last_name": user.LastName, "username": user.Username, "email": user.Email, "active": user.Active, "admin": user.Admin, } usersData = append(usersData, userData) } c.JSON(http.StatusOK, gin.H{ "status": http.StatusOK, "users": usersData, }) } }
Protobuf定义
syntax = "proto3"; option go_package = "./proto"; import "google/protobuf/empty.proto"; service UserService { rpc GetUsers(google.protobuf.Empty) returns (GetUsersResponse); } message User { string first_name = 1; string last_name = 2; string username = 3; string email = 4; int64 active = 5; int64 admin = 6; } message GetUsersResponse { repeated User users = 1; }
解决方案分析
一、完全可以避免双重循环
不用依赖服务端流,通过调整代码逻辑就能省去两次手动循环:
服务端取消转换循环
如果业务层的User结构体和proto生成的pb.User字段完全对应,直接修改业务层的GetUsers方法,让它返回[]*pb.User类型,这样服务端拿到结果后直接包装进GetUsersResponse即可,不需要循环转换:// 修改业务层方法的返回类型 func (s *userService) GetUsers() ([]*pb.User, error) { // 从数据库查询后直接构造pb.User对象 var pbUsers []*pb.User // 数据库查询逻辑... for _, dbUser := range dbQueryResult { pbUsers = append(pbUsers, &pb.User{ FirstName: dbUser.FirstName, LastName: dbUser.LastName, Username: dbUser.Username, Email: dbUser.Email, Active: int64(dbUser.Active), Admin: int64(dbUser.Admin), }) } return pbUsers, nil }对应的服务端gRPC方法就可以简化成:
func (s *usersGRPCServer) GetUsers(ctx context.Context, empty *empty.Empty) (*pb.GetUsersResponse, error) { users, err := s.service.GetUsers() if err != nil { log.Println(err) return nil, status.Error(codes.Internal, "Internal Server Error") } return &pb.GetUsersResponse{Users: users}, nil }客户端取消转换循环
Protobuf生成的结构体原生支持JSON序列化(需要导入google.golang.org/protobuf/encoding/protojson),直接把gRPC返回的结果序列化成JSON即可,不用手动转成map:// 客户端替换原循环转换逻辑 jsonBytes, err := protojson.Marshal(resp) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status": http.StatusInternalServerError, "message": "序列化失败"}) return } // 解析成map后返回,或者直接返回JSON字节 var respMap map[string]interface{} json.Unmarshal(jsonBytes, &respMap) c.JSON(http.StatusOK, gin.H{ "status": http.StatusOK, "users": respMap["users"], })
二、一元API vs 服务端流的选择
优先选一元API
- 逻辑简单,一次请求一次响应,维护成本低,适合数据量不大的场景。
- 结合上面的优化,完全能做到零手动循环,性能和服务端流差距极小。
服务端流适合特定场景
- 当用户数据量极大(比如上万条),服务端一次性返回会占用大量内存时,用服务端流可以分批发送数据,客户端边接收边处理(比如接收一个
pb.User就序列化一个,直接拼进JSON数组返回),确实能把转换合并成一次循环。 - 但服务端流会增加协议复杂度,需要处理流式调用的上下文管理、中途错误中断等问题,小数据量场景没必要折腾。
- 当用户数据量极大(比如上万条),服务端一次性返回会占用大量内存时,用服务端流可以分批发送数据,客户端边接收边处理(比如接收一个
内容的提问来源于stack exchange,提问作者Jessica
相关产品推荐
相关产品推荐

