go-kratos / kratos

Your ultimate Go microservices framework for the cloud-native era.
https://go-kratos.dev
MIT License
23.38k stars 4.01k forks source link

[Question] 请问如何设置 proto3 默认值,零值字段被忽略,导致 http api 接口字段缺失 #1952

Closed mowocc closed 2 years ago

mowocc commented 2 years ago

请问如何设置 proto3 默认值,零值字段被忽略,导致 api 接口字段缺失。 比如用户余额接口,余额为0时(balance float64),接口余额字段缺失,这在对外提供的 http api 接口情况下,是无法接受。 请问在 kratos 中有解决方案吗?

fifsky commented 2 years ago

不会啊,kratos里面默认是发射proto协议的零值,配置如下 https://github.com/go-kratos/kratos/blob/main/encoding/json/json.go#L18 除非你用的不是kratos的encoding json

mowocc commented 2 years ago

因为自定义了 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 的其它模块设计中也同样存在。

daemon365 commented 2 years ago

因为自定义了 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
   }),
shenqidebaozi commented 2 years ago

kratos并没有写死结构,是你自己的 proto 声明,proto生成出来的struct结构体本来就带省略的tag,你自己包装结构体没有使用protojson,不是本就如此吗。我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题。可以考虑拼接字符串,或者在网关上加通用的结构,通过网关去做结构上的兼容

mowocc commented 2 years ago

因为自定义了 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
   }),

感谢回复,这也是一种解决方案

mowocc commented 2 years ago

我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题

拼接byte数组的方式已经理解,https://github.com/go-kratos/gateway 可以添加通用结构吗? 请问有比较通用的处理方式吗?

mowocc commented 2 years ago

kratos并没有写死结构,是你自己的 proto 声明,proto生成出来的struct结构体本来就带省略的tag,你自己包装结构体没有使用protojson,不是本就如此吗。我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题。可以考虑拼接字符串,或者在网关上加通用的结构,通过网关去做结构上的兼容

感谢回复,最终采用了字符串拼接的方式。

guihouchang commented 2 years ago
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
}

这样子会不会好一点的呢? 只不过要多进行一次序列化

Xwudao commented 2 years ago
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
}

这样子会不会好一点的呢? 只不过要多进行一次序列化

还是有零值问题呀

guihouchang commented 2 years ago
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,
    }
shenqidebaozi commented 2 years ago

image

doing-cr7 commented 1 year ago

20230329 网上的方法都试遍了 还是使用这个字节拼接的方式 靠谱

kratos-ci-bot commented 1 year ago

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

zongjiye commented 1 year ago

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
    }
}```

看了一下上面还是觉得字符拼接或者替换还是更靠谱些,改成替换优雅点
jmolboy commented 1 year ago
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": {}
}
FarmerChillax commented 1 year ago

https://go-kratos.dev/docs/intro/faq#11%E6%8E%A7%E5%88%B6-http-%E8%BF%94%E5%9B%9E-enum%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%90%8D%E7%A7%B0%E5%92%8C%E6%95%B0%E5%AD%97

kvii commented 12 months ago

中间件定义看我这个就行了,嘎嘎好使。

不过有一说一,能从 "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"`
}
JefferyWang commented 5 months ago

自定义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,
    })
}