Closed mowocc closed 2 years ago
不会啊,kratos里面默认是发射proto协议的零值,配置如下 https://github.com/go-kratos/kratos/blob/main/encoding/json/json.go#L18 除非你用的不是kratos的encoding json
因为自定义了 ResponseEncoder,kratos的encoding json会选择使用默认的encoding/json,情况类似:https://github.com/go-kratos/kratos/issues/1539
最后的解决方案是:自定义 Response struct 同样使用 proto3 定义,data
定义为 google.protobuf.Any
,xxxReply 赋值给 data
message Response {
int32 code = 1;
string message = 2;
google.protobuf.Any data = 3;
}
代码大致如下:
func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
codec, _ := kHttp.CodecForRequest(r, "Accept")
rsp := &Response{
Code: http.StatusOK,
Message: http.StatusText(http.StatusOK),
//Data: v,
}
if m, ok := v.(proto.Message); ok {
any, err := anypb.New(m)
if err != nil {
return err
}
rsp.Data = any
}
data, err := MarshalOptions.Marshal(rsp)
if err != nil {
return err
}
w.Header().Set("Content-Type", ContentType(codec.Name()))
_, err = w.Write(data)
if err != nil {
return err
}
return nil
}
但这种方案不够优雅,同时 data 中会出现 "@type": "type.googleapis.com/api.order.v3.OrderReply"
字段,是否有更好的解决方案?
同时思考一个问题,在项目 Response 结构已经确定,无法修改的情况下,kratos 写死结构的做法确实不够灵活,即使这是使用 google 推荐的方式,但在对接遗留的老项目、或者考虑接口兼容性,这样的情况会让大部分项目放弃使用 kratos,这种情况在 kratos 的其它模块设计中也同样存在。
因为自定义了 ResponseEncoder,kratos的encoding json会选择使用默认的encoding/json,情况类似:#1539 最后的解决方案是:自定义 Response struct 同样使用 proto3 定义,data 定义为 google.protobuf.Any,xxxReply 赋值给 data 代码大致如下:
func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error { codec, _ := kHttp.CodecForRequest(r, "Accept") rsp := &Response{ Code: http.StatusOK, Message: http.StatusText(http.StatusOK), //Data: v, } if m, ok := v.(proto.Message); ok { any, err := anypb.New(m) if err != nil { return err } rsp.Data = any } data, err := MarshalOptions.Marshal(rsp) if err != nil { return err } w.Header().Set("Content-Type", ContentType(codec.Name())) _, err = w.Write(data) if err != nil { return err } return nil }
但这种方案不够优雅,同时 data 中会出现
"@type": "type.googleapis.com/api.order.v3.OrderReply"
字段,是否有更好的解决方案?同时思考一个问题,在项目 Response 结构已经确定,无法修改的情况下,kratos 写死结构的做法确实不够灵活,即使这是使用 google 推荐的方式,但在对接遗留的老项目、或者考虑接口兼容性,这样的情况会让大部分项目放弃使用 kratos,这种情况在 kratos 的其它模块设计中也同样存在。
这个是 protobuf
的问题 我们也想解决,但是解决不掉。
目前提供一种拼接byte
数组的方式 可以参考一下
func(w nethttp.ResponseWriter, r *nethttp.Request, v interface{}) error {
codec, _ := http.CodecForRequest(r, "Accept")
data, err := codec.Marshal(v)
if err != nil {
return err
}
w.WriteHeader(nethttp.StatusOK)
var reply *ResponseBody
if err == nil {
reply = &ResponseBody{
Code: 0,
Msg: "",
}
} else {
reply = &ResponseBody{
Code: 500,
Msg: "",
}
var target *errors.Error
if errors.As(err, target) {
reply.Code = int32(target.Code)
reply.Msg = target.Message
}
}
replyData, err := codec.Marshal(reply)
if err != nil {
return err
}
var newData = make([]byte, 0, len(replyData)+len(data)+8)
newData = append(newData, replyData[:len(replyData)-1]...)
newData = append(newData, []byte(`,"data":`)...)
newData = append(newData, data...)
newData = append(newData, '}')
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(newData)
if err != nil {
return err
}
return nil
}),
kratos并没有写死结构,是你自己的 proto 声明,proto生成出来的struct结构体本来就带省略的tag,你自己包装结构体没有使用protojson,不是本就如此吗。我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题。可以考虑拼接字符串,或者在网关上加通用的结构,通过网关去做结构上的兼容
因为自定义了 ResponseEncoder,kratos的encoding json会选择使用默认的encoding/json,情况类似:#1539 最后的解决方案是:自定义 Response struct 同样使用 proto3 定义,data 定义为 google.protobuf.Any,xxxReply 赋值给 data 代码大致如下:
func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error { codec, _ := kHttp.CodecForRequest(r, "Accept") rsp := &Response{ Code: http.StatusOK, Message: http.StatusText(http.StatusOK), //Data: v, } if m, ok := v.(proto.Message); ok { any, err := anypb.New(m) if err != nil { return err } rsp.Data = any } data, err := MarshalOptions.Marshal(rsp) if err != nil { return err } w.Header().Set("Content-Type", ContentType(codec.Name())) _, err = w.Write(data) if err != nil { return err } return nil }
但这种方案不够优雅,同时 data 中会出现
"@type": "type.googleapis.com/api.order.v3.OrderReply"
字段,是否有更好的解决方案? 同时思考一个问题,在项目 Response 结构已经确定,无法修改的情况下,kratos 写死结构的做法确实不够灵活,即使这是使用 google 推荐的方式,但在对接遗留的老项目、或者考虑接口兼容性,这样的情况会让大部分项目放弃使用 kratos,这种情况在 kratos 的其它模块设计中也同样存在。这个是
protobuf
的问题 我们也想解决,但是解决不掉。 目前提供一种拼接byte
数组的方式 可以参考一下func(w nethttp.ResponseWriter, r *nethttp.Request, v interface{}) error { codec, _ := http.CodecForRequest(r, "Accept") data, err := codec.Marshal(v) if err != nil { return err } w.WriteHeader(nethttp.StatusOK) var reply *ResponseBody if err == nil { reply = &ResponseBody{ Code: 0, Msg: "", } } else { reply = &ResponseBody{ Code: 500, Msg: "", } var target *errors.Error if errors.As(err, target) { reply.Code = int32(target.Code) reply.Msg = target.Message } } replyData, err := codec.Marshal(reply) if err != nil { return err } var newData = make([]byte, 0, len(replyData)+len(data)+8) newData = append(newData, replyData[:len(replyData)-1]...) newData = append(newData, []byte(`,"data":`)...) newData = append(newData, data...) newData = append(newData, '}') w.Header().Set("Content-Type", "application/json") _, err = w.Write(newData) if err != nil { return err } return nil }),
感谢回复,这也是一种解决方案
我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题
拼接byte数组的方式已经理解,https://github.com/go-kratos/gateway 可以添加通用结构吗? 请问有比较通用的处理方式吗?
kratos并没有写死结构,是你自己的 proto 声明,proto生成出来的struct结构体本来就带省略的tag,你自己包装结构体没有使用protojson,不是本就如此吗。我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题。可以考虑拼接字符串,或者在网关上加通用的结构,通过网关去做结构上的兼容
感谢回复,最终采用了字符串拼接的方式。
type Response struct {
Code int `json:"code"`
Message string `json:"msg"`
Data interface{} `json:"data"`
}
func ResponseEncoder(w stdHttp.ResponseWriter, r *stdHttp.Request, v interface{}) error {
reply := &Response{}
reply.Code = 20000
reply.Message = "success"
codec, _ := http.CodecForRequest(r, "Accept")
data, err := codec.Marshal(v)
_ = json.Unmarshal(data, &reply.Data)
if err != nil {
return err
}
data, err = codec.Marshal(reply)
if err != nil {
return err
}
w.Header().Set("Content-Type", contentType(codec.Name()))
w.WriteHeader(stdHttp.StatusOK)
w.Write(data)
return nil
}
这样子会不会好一点的呢? 只不过要多进行一次序列化
type Response struct { Code int `json:"code"` Message string `json:"msg"` Data interface{} `json:"data"` } func ResponseEncoder(w stdHttp.ResponseWriter, r *stdHttp.Request, v interface{}) error { reply := &Response{} reply.Code = 20000 reply.Message = "success" codec, _ := http.CodecForRequest(r, "Accept") data, err := codec.Marshal(v) _ = json.Unmarshal(data, &reply.Data) if err != nil { return err } data, err = codec.Marshal(reply) if err != nil { return err } w.Header().Set("Content-Type", contentType(codec.Name())) w.WriteHeader(stdHttp.StatusOK) w.Write(data) return nil }
这样子会不会好一点的呢? 只不过要多进行一次序列化
还是有零值问题呀
type Response struct { Code int `json:"code"` Message string `json:"msg"` Data interface{} `json:"data"` } func ResponseEncoder(w stdHttp.ResponseWriter, r *stdHttp.Request, v interface{}) error { reply := &Response{} reply.Code = 20000 reply.Message = "success" codec, _ := http.CodecForRequest(r, "Accept") data, err := codec.Marshal(v) _ = json.Unmarshal(data, &reply.Data) if err != nil { return err } data, err = codec.Marshal(reply) if err != nil { return err } w.Header().Set("Content-Type", contentType(codec.Name())) w.WriteHeader(stdHttp.StatusOK) w.Write(data) return nil }
这样子会不会好一点的呢? 只不过要多进行一次序列化
还是有零值问题呀
这个利用到pbjson库,但是前提要设置
json.MarshalOptions = protojson.MarshalOptions{
EmitUnpopulated: true,
UseProtoNames: true,
}
20230329 网上的方法都试遍了 还是使用这个字节拼接的方式 靠谱
Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑🤝🧑👫🧑🏿🤝🧑🏻👩🏾🤝👨🏿👬🏿
20230329 I have tried all the methods on the Internet, but I still use this byte splicing method, which is reliable
type httpResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// ApiErrorEncoder 错误响应封装
func ApiErrorEncoder() http.EncodeErrorFunc {
return func(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {
if err == nil {
return
}
se := &httpResponse{}
gs, ok := status.FromError(err)
if !ok {
se = &httpResponse{Code: stdhttp.StatusInternalServerError}
}
se = &httpResponse{
Code: httpstatus.FromGRPCCode(gs.Code()),
Message: gs.Message(),
Data: nil,
}
codec, _ := http.CodecForRequest(r, "Accept")
body, err := codec.Marshal(se)
if err != nil {
w.WriteHeader(stdhttp.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/"+codec.Name())
w.WriteHeader(se.Code)
_, _ = w.Write(body)
}
}
// ApiResponseEncoder 请求响应封装
func ApiResponseEncoder() http.EncodeResponseFunc {
return func(w stdhttp.ResponseWriter, r *stdhttp.Request, v interface{}) error {
if v == nil {
return nil
}
resp := &httpResponse{
Code: stdhttp.StatusOK,
Message: stdhttp.StatusText(stdhttp.StatusOK),
}
codec := encoding.GetCodec("json")
respData, err := codec.Marshal(resp)
if err != nil {
return err
}
data, err := codec.Marshal(v)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(stdhttp.StatusOK)
_, err = w.Write(bytes.Replace(respData, []byte("null"), data, 1))
if err != nil {
return err
}
return nil
}
}```
看了一下上面还是觉得字符拼接或者替换还是更靠谱些,改成替换优雅点
type httpResponse struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data"` } // ApiErrorEncoder 错误响应封装 func ApiErrorEncoder() http.EncodeErrorFunc { return func(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) { if err == nil { return } se := &httpResponse{} gs, ok := status.FromError(err) if !ok { se = &httpResponse{Code: stdhttp.StatusInternalServerError} } se = &httpResponse{ Code: httpstatus.FromGRPCCode(gs.Code()), Message: gs.Message(), Data: nil, } codec, _ := http.CodecForRequest(r, "Accept") body, err := codec.Marshal(se) if err != nil { w.WriteHeader(stdhttp.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/"+codec.Name()) w.WriteHeader(se.Code) _, _ = w.Write(body) } } // ApiResponseEncoder 请求响应封装 func ApiResponseEncoder() http.EncodeResponseFunc { return func(w stdhttp.ResponseWriter, r *stdhttp.Request, v interface{}) error { if v == nil { return nil } resp := &httpResponse{ Code: stdhttp.StatusOK, Message: stdhttp.StatusText(stdhttp.StatusOK), } codec := encoding.GetCodec("json") respData, err := codec.Marshal(resp) if err != nil { return err } data, err := codec.Marshal(v) if err != nil { return err } w.Header().Set("Content-Type", "application/json") w.WriteHeader(stdhttp.StatusOK) _, err = w.Write(bytes.Replace(respData, []byte("null"), data, 1)) if err != nil { return err } return nil } }``` 看了一下上面还是觉得字符拼接或者替换还是更靠谱些,改成替换优雅点
我这里报异常:
{
"code": 500,
"reason": "",
"message": "failed to marshal, message is *server.httpResponse, want proto.Message",
"metadata": {}
}
中间件定义看我这个就行了,嘎嘎好使。
不过有一说一,能从 "code message data" 格式中迁移出来的就尽量迁移出来吧,搞那些玩意儿干啥。每个后端的 "code message data" 的定义都不一样,连累前端挨个写 axios 中间件。都用上 kratos 了,这些老观念该抛弃就抛弃了吧。
package server
import (
v1 "code_msg_data/api/helloworld/v1"
"code_msg_data/internal/conf"
"code_msg_data/internal/service"
sj "encoding/json"
nt "net/http"
"strings"
"github.com/go-kratos/kratos/v2/encoding"
"github.com/go-kratos/kratos/v2/encoding/json"
"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/transport/http"
)
// 最终效果
// $ curl http://localhost:8000/helloworld/kvii
// {"code":0,"message":"success","data":{"message":"Hello kvii"}}
// $ curl http://localhost:8000/helloworld/err
// {"code":404,"message":"user not found"}
// NewHTTPServer new an HTTP server.
func NewHTTPServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *http.Server {
var opts = []http.ServerOption{
http.Middleware(
recovery.Recovery(),
),
http.ErrorEncoder(DefaultErrorEncoder), // <- 关键代码
http.ResponseEncoder(DefaultResponseEncoder), // <- 关键代码
}
if c.Http.Network != "" {
opts = append(opts, http.Network(c.Http.Network))
}
if c.Http.Addr != "" {
opts = append(opts, http.Address(c.Http.Addr))
}
if c.Http.Timeout != nil {
opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))
}
srv := http.NewServer(opts...)
v1.RegisterGreeterHTTPServer(srv, greeter)
return srv
}
// DefaultResponseEncoder copy from http.DefaultResponseEncoder
func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
if v == nil {
return nil
}
if rd, ok := v.(http.Redirector); ok {
url, code := rd.Redirect()
nt.Redirect(w, r, url, code)
return nil
}
codec := encoding.GetCodec(json.Name) // ignore Accept Header
data, err := codec.Marshal(v)
if err != nil {
return err
}
bs, _ := sj.Marshal(NewResponse(data))
w.Header().Set("Content-Type", ContentType(codec.Name()))
_, err = w.Write(bs)
if err != nil {
return err
}
return nil
}
// DefaultErrorEncoder copy from http.DefaultErrorEncoder.
func DefaultErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
se := FromError(errors.FromError(err)) // change error to BaseResponse
codec := encoding.GetCodec(json.Name) // ignore Accept header
body, err := codec.Marshal(se)
if err != nil {
w.WriteHeader(nt.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", ContentType(codec.Name()))
// w.WriteHeader(int(se.Code)) // ignore http status code
_, _ = w.Write(body)
}
const (
baseContentType = "application"
)
// ContentType returns the content-type with base prefix.
func ContentType(subtype string) string {
return strings.Join([]string{baseContentType, subtype}, "/")
}
func NewResponse(data []byte) BaseResponse {
return BaseResponse{
Code: 0,
Message: "success",
Data: sj.RawMessage(data),
}
}
func FromError(e *errors.Error) *BaseResponse {
if e == nil {
return nil
}
return &BaseResponse{
Code: e.Code,
Message: e.Message,
}
}
type BaseResponse struct {
Code int32 `json:"code"`
Message string `json:"message"`
Data sj.RawMessage `json:"data,omitempty"`
}
自定义MarshalJSON
package cloudapiv3
import (
"encoding/json"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
// Error 实现了
type Error struct {
Code string `json:"Code"`
Message string `json:"Message"`
}
type Response struct {
RequestId string `json:"RequestId"`
Error *Error `json:"Error,omitempty"`
Data any `json:"Data,omitempty"`
}
type CloudAPIV3Response struct {
Response *Response `json:"Response"`
}
var (
// MarshalOptions is a configurable JSON format marshaller.
MarshalOptions = protojson.MarshalOptions{
EmitUnpopulated: true,
}
)
// MarshalJSON 序列化
func (resp *Response) MarshalJSON() ([]byte, error) {
var dataRaw json.RawMessage
var err error
switch m := resp.Data.(type) {
case proto.Message:
dataRaw, err = MarshalOptions.Marshal(m)
default:
dataRaw, err = json.Marshal(m)
}
if err != nil {
return nil, err
}
return json.Marshal(&struct {
RequestId string `json:"RequestId"`
Error *Error `json:"Error,omitempty"`
Data json.RawMessage `json:"Data,omitempty"`
}{
RequestId: resp.RequestId,
Error: resp.Error,
Data: dataRaw,
})
}
请问如何设置 proto3 默认值,零值字段被忽略,导致 api 接口字段缺失。 比如用户余额接口,余额为0时(balance float64),接口余额字段缺失,这在对外提供的 http api 接口情况下,是无法接受。 请问在 kratos 中有解决方案吗?