业务要求返回json,但是服务内部使用的是proto协议,故需要将proto协议转为json格式。
先确定需求,我们要求的返回类型是 code, msg, data. 返回数据放在data里面。
这里方案总共有四种:
方案1:
- 下级服务返回protoData给网关,得到 []byte
- 根据[]byte解码proto结构, 直接解析到json返回结构体的Data成员变量
- 编码Json,返回数据
这个方案存在的问题就是说,json的编码会根据omitempty字段而不展示空的字段。
比如proto结构里面有默认字段,就不会展示,这种情况不满足需求。
方案2:
- 下级服务返回protoData给网关,得到 []byte
- 将下级服务返回的protoData解码,得到Proto数据结构
- 使用 jsonpb库,将Proto数据结构编码为Json字符串(叫它JsonData吧)。
- Json返回结构体的Data成员类型为 json.RawMessage,将JsonData赋值给Data成员变量
- 编码Json,返回数据
这里步骤3涉及到三个参数,满足了展示默认字段的需求。
但是相比方案2,它进行了三次数据结构的转换,一次数据copy。
方案3:
- 修改与下级服务通信proto文件,增加用于网关返回的结构体叫它JsonResponse
- 将下级服务传递的 []byte 直接解码到JsonResponse.Data成员变量上
- 将JsonResponse编码为json得到返回的[]byte, 返回数据
相比方案2,我们采用直接解码proto再编码proto为json的方案,减少了一次数据结构转化,一次数据Copy。
计算次数与方案1差不多,但是满足了默认字段展示的需求。
基于方案3,拓展方案4:
- 下级服务直接返回JsonResponse
这里我认为性能不如方案3,理由如下:
- 分析方案3,其实是[]byte->proto->json,两次转换
- 下级服务返回JsonResponse之后,我们还是要做[]byte->proto->json两次转换
- 下级服务返回的字段相比之前更多了,可以认为增加了网络开销
- 下级服务返回的结构体更大,编解码时耗时更多了
综上,我认为方案3是我们的最优解。
相比方案1在解决了后台提出的空字段不展示的问题的基础上,没有额外编解码性能开销。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
// []byte 转 RoomCenterResponse jsonResponse.Code = result if err := protoV2.Unmarshal(response, jsonResponse.Data); err != nil { slog.LOG_ERROR("RoomCenterResponse proto Unmarshal error, err: %v.", err) httpMsg.ResponseError(w, int32(GrpcProtos.ResultType_ERR_Decode_Response), string("RoomCenterResponse proto Unmarshal error.")) return } httpMsg.ResponseProto(w, jsonResponse) return |
ResponseProto就是 proto->json的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// ResponseProto 将Proto结构作为Data返回 func (httpMsg *HttpMessage) ResponseProto( w http.ResponseWriter, protoMessage proto.Message, ) { // Proto 转 Json jsonpbMarshaler := &jsonpb.Marshaler{ EnumsAsInts: true, // 是否将枚举值设定为整数,而不是字符串类型 EmitDefaults: true, // 是否将字段值为空的渲染到JSON结构中 OrigName: true, // 是否使用原生的proto协议中的字段 } jsonData, err := jsonpbMarshaler.MarshalToString(protoMessage) if err != nil { slog.LOG_ERROR("jsonpbMarshaler Proto Marshal To Json error, err: %v.", err) httpMsg.ResponseError(w, int32(GrpcProtos.ResultType_ERR_Decode_Response), string("jsonpbMarshaler Proto Marshal To Json error.")) return } w.Write([]byte(jsonData)) return } |
这里JsonResponse对象是这样定义的:
1 2 3 4 5 6 7 8 9 |
// gateway 返回结构 message OnlineCenterJsonResponse { int32 code = 1; string msg = 2; OnlineCenterResponse data = 3; } jsonResponse := &GateWayProtos.OnlineCenterJsonResponse{} jsonResponse.Data = &GateWayProtos.OnlineCenterResponse{} |
这样相当于每个请求多了一个结构,增加了代码量,但是提升了性能。
我是来还愿的,经过一段时间的开发,还是觉得 JsonResponse的Proto结构不好用,代码量大也不舒服.
还是决定采用方案2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
// HTTPMessage Json Response type jsonResponse struct { Code int32 `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data"` } type emptyData struct{} // responseError 返回Json结构 func (httpMsg *HttpMessage) responseError( w http.ResponseWriter, code int32, msg string, ) { httpMsg.responseJson(w, code, msg, &emptyData{}) } // responseData 返回Json结构 func (httpMsg *HttpMessage) responseData( w http.ResponseWriter, code int32, msg string, protoMessage proto.Message, ) { json_raw, err := httpMsg.pb2jsonRaw(protoMessage) if err != nil { result := int32(GrpcProtos.ResultType_ERR_Encode_Response) httpMsg.responseError(w, result, err.Error()) return } httpMsg.responseJson(w, code, msg, json_raw) } // responseJson 返回Json格式信息 func (httpMsg *HttpMessage) responseJson( w http.ResponseWriter, code int32, msg string, data interface{}, ) { jsonResponse := &jsonResponse{ Code: code, Msg: msg, Data: data, } // Data应保证不为nil, 否则返回的Json不符合要求. if jsonResponse.Data == nil { jsonResponse.Data = &emptyData{} } response, err := json.Marshal(jsonResponse) if err != nil { slog.LOG_ERROR("jsonResponse Marshal Json error, err: %v.", err) w.Write([]byte("{\"code\":1,\"msg\":\"jsonResponse Marshal Json error.\",\"data\":{}}")) return } w.Write(response) } // pb2json_raw 将Proto结构编码为 json.RawMessage func (httpMsg *HttpMessage) pb2jsonRaw( protoMessage proto.Message, ) (json.RawMessage, error) { // Proto 转 Json jsonpbMarshaler := &jsonpb.Marshaler{ EnumsAsInts: true, // 是否将枚举值设定为整数,而不是字符串类型 EmitDefaults: true, // 是否将字段值为空的渲染到JSON结构中 OrigName: true, // 是否使用原生的proto协议中的字段 } data, err := jsonpbMarshaler.MarshalToString(protoMessage) if err != nil { slog.LOG_ERROR("pb2jsonRaw MarshalToString error, err: %v.", err) return nil, err } // 将Json作为RawMessage返回 return json.RawMessage(data), nil } |
这个大概就是所有代码. 日志部分可以忽略掉。
【golang】proto转json方案