V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
ljzxloaf
V2EX  ›  程序员

protobuf 不支持泛型?

  •  
  •   ljzxloaf · 2 天前 · 3572 次点击

    有人提 proposal 被拒了: https://github.com/protocolbuffers/protobuf/issues/9527#issue-1142821422

    那像这种情况怎么搞?每个 response 都创建一个 wrapper ?

    public class Response<T>  {
    
        @NotNull
        private ResponseStatus status;
    
        private T result;
    
     }
    
    57 条回复    2025-03-27 14:18:26 +08:00
    a33291
        1
    a33291  
       2 天前
    是的,非常恶心
    也不支持继承
    或者写 t4 生成😂
    ljzxloaf
        2
    ljzxloaf  
    OP
       2 天前
    @a33291 #1 写 t4 生成是啥意思
    gam2046
        3
    gam2046  
       2 天前
    message {
    bytes result = 1;
    }

    自己根据类型,将 result 解析成正确的类型。
    sujin190
        4
    sujin190  
       2 天前 via Android
    proto 作为协议数据声明,支持范型意味着声明侵入编码和传输过程,本来就不合适。不支持才是合理的。否则人家 isser 说了使用 any 类型就好了啊
    debuggerx
        5
    debuggerx  
       2 天前   ❤️ 2
    谁叫好好的响应数据非要包一层呢
    Rickkkkkkk
        6
    Rickkkkkkk  
       2 天前   ❤️ 2
    用 string

    支持泛型什么鬼,那和不支持泛型的语言怎么交互?
    nonempty
        7
    nonempty  
       2 天前
    byte ?
    ljzxloaf
        8
    ljzxloaf  
    OP
       2 天前
    @sujin190 #4 怎么定义协议?泛型说白了也就是个 message ,只不过是个动态的 message 而已。而且我觉得最恶心的一点是他们不仅不愿意支持这功能,而且还不愿意接受外部设计,连提 pr 的机会都不给。
    ljzxloaf
        9
    ljzxloaf  
    OP
       2 天前
    @Rickkkkkkk #6 只是支持而已,你可以不用,也可以生成不含泛型的 sdk
    ljzxloaf
        10
    ljzxloaf  
    OP
       2 天前
    @debuggerx #5 我在工作中看到的响应体都是这样设计的,看下你们的设计?
    Rickkkkkkk
        11
    Rickkkkkkk  
       2 天前
    @ljzxloaf 我确实没想到在一个不支持泛型的语言里要怎么用这个协议。这复杂度值得吗?
    ljzxloaf
        12
    ljzxloaf  
    OP
       2 天前
    @Rickkkkkkk #11 没有泛型的语言直接去掉泛型就好了,泛型只是个语法糖
    sujin190
        13
    sujin190  
       2 天前   ❤️ 4
    @ljzxloaf #8 协议只是约定,好比 protobuf 用 id 号指定字段,这个字段的类型只是双方约定信息,实际数据中不包含这个类型信息也不应该包含,你要加泛型就要在实际数据中包含类型信息,而这个类型实现过程各个语言支持不一样,已经产生了平台依赖,破坏了协议应该和实际实现无关的只是抽象约定的逻辑

    如果你想说泛型类型信息也可以是双方约定,那么这和你申明类型是 any 然后任然需要另外一个字段在实际数据传输类型标记有何区别,如果你想说为何不可以把这个过程实现在基础库里,逻辑就是这个不符合基础库不应该包含非原子操作,好比 cpu 机器码不应该包含一个读取文件的高等级 api 一个逻辑,你想要你自己封装扩展就是了

    不要把接口设计和程序逻辑混在一起,理解不到位也不要怪别人
    Rickkkkkkk
        14
    Rickkkkkkk  
       2 天前
    @ljzxloaf 去掉泛型,它的类型是什么?生成完的代码长啥样?而且这是藏逻辑,并不是好设计。
    brightguo
        15
    brightguo  
       2 天前
    对,默认不支持。
    我这边用的 golang ,我就把他改了,让 google.protobuf.Any 类型转换成了 interface{}。
    你可以借鉴下( https://github.com/guoming0000/protobuf-go/commit/628f6211b3a75c96181c5137b1605fcfcedaaa4a ),然后改下 protobuf-java ,看看能不能满足你的需求。
    不过这么改了就是非标了
    wunonglin
        16
    wunonglin  
       2 天前   ❤️ 1
    ljzxloaf
        17
    ljzxloaf  
    OP
       2 天前
    @Rickkkkkkk #14 取舍问题,openapi 就支持了
    debuggerx
        18
    debuggerx  
       2 天前
    @ljzxloaf 很简单,正常响应直接返回数据,http 状态吗 200 ;接口出错时,前端参数问题就用 400 状态码,服务器问题就用 500 ,然后响应结构:{"code": 业务定义的错误码, "msg": 错误描述或希望显示的文案}

    前端 api 设计形式类似 static fetchItemById = (id: number, onError: (code: number, msg: string) => void): Promise<Item | null> => {...}

    直接根据 http 状态码去解析,而不是先解析,再根据状态搞泛型
    ljzxloaf
        19
    ljzxloaf  
    OP
       2 天前
    @wunonglin #16 没看懂,但是他这个问题不是加个 optional 就能进解决吗。。
    eslizn
        20
    eslizn  
       2 天前
    这不是 oneof 的场景吗?
    ljzxloaf
        21
    ljzxloaf  
    OP
       2 天前
    @debuggerx #18 这只适用于 http 接口,而且即使是 http 接口,我也认为这样并不好,把业务状态放在协议里了
    ljzxloaf
        22
    ljzxloaf  
    OP
       2 天前
    @brightguo #15 搞不了搞不了...没那个能力...我还是老老实实多写几个 wrapper 吧
    debuggerx
        23
    debuggerx  
       2 天前
    @ljzxloaf 并非只适用 http ,而是一种设计思路,websocket 、mqtt 也都可以用类似的设计。而且这并不是什么把业务状态放在协议里,http 协议的状态码本就希望服务器能够表达一些明确的意图,不管成功失败全用 200 才是自作聪明的反设计,曾经可能还有些场景有些理由这样做,现在可以扔掉这种惯性设计了。
    lesismal
        24
    lesismal  
       2 天前   ❤️ 5
    > 而且我觉得最恶心的一点是他们不仅不愿意支持这功能,而且还不愿意接受外部设计,连提 pr 的机会都不给。

    别人合理拒绝,却反过来说别人封闭,什么道理!
    要都是随便接受外部设计和 pr ,各种项目早都得被乌合之众搞凉了!
    iyaozhen
        25
    iyaozhen  
       2 天前
    protobuf 肯定不要支持泛型呀
    它一个二进制格式描述协议,和泛型有毛关系

    你 pb idl 怎么写的,贴一下呢。就是需要 Response1 、Response2 一个个描述
    如果你想 result 一会儿是这个 一会儿是那个,那这个场景就不适合用 pb 。直接裸写就行了,然后用 Swagger 的 Inheritance and Polymorphism 能力
    ljzxloaf
        26
    ljzxloaf  
    OP
       2 天前
    @lesismal #24 好吧,我表达的有问题,不是说随便接受 pr 。开源项目的一大优势不就是可以倾听社区的声音吗?我意思是没必要这样毫无余地的就拒绝,如果社区呼声很高,也是可以考虑的吧。
    ljzxloaf
        27
    ljzxloaf  
    OP
       2 天前
    @debuggerx #23 像“余额不足”这种业务情况如何用 status 表达,不还是得用自定义的 code 和 message
    Rickkkkkkk
        28
    Rickkkkkkk  
       2 天前
    @ljzxloaf 说到取舍,与其用泛型,这里最好的设计就是字段是 string 。这个取舍稍微想想就能选对了。
    debuggerx
        29
    debuggerx  
       2 天前 via Android
    @ljzxloaf http 状态码 500 ,返回数据类似{"code": 610016, "msg": "余额不足"}
    简单来说就是成功时 http 状态 200 ,响应数据直接是业务负载数据,错误时 http 状态非 200 ,然后响应数据返回业务错误码和信息文本
    Trim21
        30
    Trim21  
       2 天前
    @debuggerx 是具体是哪个 4xx 可能还可需要讨论,但肯定不该是 http 500 ...
    moudy
        31
    moudy  
       2 天前
    @ljzxloaf #9 问题就在这"可“字上。项目集成,维护,切换组件过程中麻烦事就够多了。

    好比别人 boolean 都是 true false, 你的项目为了灵活定义 boolean 可以是 true false ""。代码都是 python 写的毫无问题,跟 js 交互也没问题。现在要跟 java 交互,项目组肯定日常问候。
    guanzhangzhang
        32
    guanzhangzhang  
       1 天前
    pb 这种就是不支持泛型的,就像 http 里一个接口返回的 json 内容里有个 list 的值可能为
    [1,2,3,4]和["1","2","3"]的,你序列化难道不是要 case int 和 string 吗,这样叫泛型吗,泛型是编译推导代码内的类型后去做类型擦除的。
    可以是任意类型的话那应该用 json rpc 啥的有个 id 和 id 对应 data body 之类的,以及使用一个 websocket 传输不一样数据结构的 socketio 协议。
    你应该考虑用 oneof 或者 message id | message body 这种。或者把那段类型指定为 byte ,自己序列化
    wolfie
        33
    wolfie  
       1 天前
    很正常的需求,不用怀疑自己。
    写过一些组件就懂泛型的意义了。
    InkStone
        34
    InkStone  
       1 天前
    @ljzxloaf 它下面不是已经说理由了么,因为泛型太复杂了。如果要做泛型,那你需要做的不只是设计一套泛型语法,写一个看起来可行的实现,而是评估泛型在每一种 pb 可能交互的主流语言中使用会出现什么问题,在实现中解决这些问题,并给出一个健壮的测试集。又不是说你随便提个 PR 别人就必须得合的……
    flyqie
        35
    flyqie  
       1 天前 via Android
    pb 不是有个类似于泛型的东西吗。

    会在输出的时候额外加 type 来做标识。

    我记错了?
    flyqie
        36
    flyqie  
       1 天前 via Android
    @wolfie #33

    有意义确实不假,但问题是太过于复杂了。

    pb 最开始走的那个以 codegen 为主的模式做泛型会很麻烦。
    jim9606
        37
    jim9606  
       1 天前
    把泛型这个需要语言特性支持的东西放进 IDL 就不合适,给不支持泛型的语言 codegen 带来一堆麻烦。
    any 或者 oneof 然后自己 cast 就好懂多了。
    楼上提到几个魔改也只是特定语言 codegen 小改,没动 IDL 。
    hyperbin
        38
    hyperbin  
       1 天前 via Android
    protobuf 的库是跨语言,那么那些不支持泛型的语言该如何适配?
    fffq
        39
    fffq  
       1 天前
    pb.Any 然后自己玩
    5261
        40
    5261  
       1 天前
    我其实也在想 pb 的优势到底是啥? 除了跨语言外,还有就是同样的内容可能字节大小变小,但是现在流量不是过剩的情况下,json 不是也都满足嘛?
    jigujigushanshan
        41
    jigujigushanshan  
       1 天前
    @5261 性能啊,解析速度快 体积小 传输速度快,相反如果加泛型 想都不用想唯一的优势都没了,那还不如 json 各种场景一把梭
    hidemyself
        42
    hidemyself  
       1 天前
    我理解出现这种场景,说明不适合用 pb
    AEnjoyable
        43
    AEnjoyable  
       1 天前
    看你要传输什么东西 正常的就 any + wrapper 包, 不然就像楼上说的 byte,然后自己反序列化
    bli22ard
        44
    bli22ard  
       1 天前   ❤️ 1
    你是先有 proto 文件,再生成数据存储类,而不是先有你发的 Response<T> 这种定义。proto 定义的可能最佳做法是,定义一个 Head message ,定义 code 和 msg ,然后 service 的方法返回值,message RespOrderList{
    Head head=1;
    repeated Order orderList=2;
    } 。protobuf 要是加个泛型,你让没有泛型的语言怎么实现。另外,即使 java 代码中使用 json , 我也不觉得,Response<T> 是一种好的做法。
    lpxxn
        45
    lpxxn  
       1 天前
    最好不要用 bytes ,调用方非常难受。
    rev1si0n
        46
    rev1si0n  
       1 天前   ❤️ 1
    这个问题,要么填 bytes 自己序列化,要么 oneof 。纠结这个说明你可能也没有深入使用也没有看看别人是怎么做的还说让人支持,你在破坏它的通用性,为了你的需求需要做两个版本的文档。建议自己搞个,最好别用,我只能说以后的坑还多了。
    MEIerer
        47
    MEIerer  
       1 天前
    这两天准备看这个来着
    itskingname
        48
    itskingname  
       1 天前
    @debuggerx 写 Java 的人就喜欢什么都包好几层,就跟他们的空文件夹嵌套几十层一样。
    securityCoding
        49
    securityCoding  
       1 天前
    可能是定位有偏差,你定义一个通用 baseResult 字段就好了,用反射来解决
    rev1si0n
        50
    rev1si0n  
       1 天前
    我觉得这个定义适合你做参考,开发的时候甚至没有那么多现成的项目可以让我参考该怎么做。https://github.com/firerpa/lamda/tree/6e1298b536d344527ddcb94e621f1b3a88aa6f32/lamda/rpc
    zmcity
        51
    zmcity  
       1 天前
    因为不是所有语言都支持泛型,如果 pb 强行兼容,在 c 语言等面向过程的语言的 sdk 里使用就会变得异常抽象。
    所以要么使用 oneof ,要么换一个完全放弃面向过程的语言的库,比如 microsoft.bond
    ljzxloaf
        52
    ljzxloaf  
    OP
       1 天前 via Android
    @lpxxn 用 byte,any 还不如多写点 boilerplate code
    lesismal
        53
    lesismal  
       1 天前
    > @lesismal #24 好吧,我表达的有问题,不是说随便接受 pr 。开源项目的一大优势不就是可以倾听社区的声音吗?我意思是没必要这样毫无余地的就拒绝,如果社区呼声很高,也是可以考虑的吧。

    基础知识储量不够但是又有兴趣的话,可以多学下,等理解不到 pb 的设计、实现、原理、做这种更改尤其是是多语言的难度和影响,你大概就不会再使用 “也就是个 message”、“只是支持而已” 这些轻浮的话了,技术的问题最好是能让自己有理有据的输出、而不是“妄加评论”

    相比于社会、生活中的很多事情,技术更严谨,无知者无畏、我穷我嗓门大我有理这种发声方式不是好的方法,你想想,自己不占理还要喷别人,跟那些非法医闹、水军舆论绑架有什么区别?

    “倾听社区的声音” 和 “倾听社区正确的声音” 是两码事。如果不懂,至少就请尊重和慎言,而不是诋毁。

    越读书越学习越觉得自己无知,共勉!
    liuidetmks
        54
    liuidetmks  
       1 天前
    用脚本生成? pmake ?
    EvaCcino
        55
    EvaCcino  
       21 小时 27 分钟前
    包一层感觉没啥意义,如果包一层都有意义的话, 为什么不直接包 1000w 层?
    ryalu
        56
    ryalu  
       8 小时 47 分钟前
    @wunonglin #16 这是 json 的,如果请求协议里是 application/proto 序列化编码方式就不行了。
    em....公司**需求要支持 json 和 proto 两种序列化方式(屎山),分享下实现(性能不算好,能用就行),在 encode 的地方动态构造 proto 对象,这样就不用所有 response 包一层了。

    ```
    package http

    import (
    "bytes"
    "fmt"
    "io"
    "log/slog"
    stdhttp "net/http"
    "reflect"
    "strconv"
    "strings"
    "sync"
    "time"

    "github.com/go-kratos/kratos/v2/encoding"
    ejson "github.com/go-kratos/kratos/v2/encoding/json"
    eproto "github.com/go-kratos/kratos/v2/encoding/proto"
    "github.com/go-kratos/kratos/v2/errors"
    "github.com/go-kratos/kratos/v2/transport/http"
    pproto "github.com/golang/protobuf/proto"
    "github.com/jhump/protoreflect/desc"
    "github.com/jhump/protoreflect/desc/builder"
    "github.com/jhump/protoreflect/dynamic"
    "google.golang.org/protobuf/encoding/protojson"
    "google.golang.org/protobuf/proto"

    ".../api/_gen/go/ecode"
    )

    var (
    messagePool = &sync.Map{}
    defaultErrorMessageDescriptor *desc.MessageDescriptor

    // MarshalOptions is a configurable JSON format marshaller.
    jsonMarshalOptions = protojson.MarshalOptions{
    EmitUnpopulated: true,
    }
    // UnmarshalOptions is a configurable JSON format parser.
    jsonUnmarshalOptions = protojson.UnmarshalOptions{
    DiscardUnknown: true,
    }

    jsonCodecHeaders = []string{"application/json", "text/json"}
    protoCodecHeaders = []string{"application/x-protobuf", "application/proto", "application/octet-stream"}
    jsonCodec = encoding.GetCodec(ejson.Name)
    protoCodec = encoding.GetCodec(eproto.Name)
    registeredCodecs = make(map[string]encoding.Codec)
    )

    func init() {
    for _, contentType := range jsonCodecHeaders {
    registeredCodecs[contentType] = jsonCodec
    }
    for _, contentType := range protoCodecHeaders {
    registeredCodecs[contentType] = protoCodec
    }

    defaultErrorMessageDescriptor, _ = builder.NewMessage("Response").
    AddField(builder.NewField("code", builder.FieldTypeInt32()).SetNumber(1)).
    AddField(builder.NewField("message", builder.FieldTypeString()).SetNumber(2)).
    AddField(builder.NewField("ts", builder.FieldTypeInt64()).SetNumber(3)).Build()
    }

    func requestDecoder(r *http.Request, v any) error {
    codec, _, ok := codecForRequest(r, "Content-Type")
    if !ok {
    return errors.BadRequest("CODEC", fmt.Sprintf("unregister Content-Type: %s", r.Header.Get("Content-Type")))
    }

    data, err := io.ReadAll(r.Body)
    if err != nil {
    return errors.BadRequest("CODEC", err.Error())
    }

    if len(data) == 0 {
    return nil
    }

    if err = codec.Unmarshal(data, v); err != nil {
    return errors.BadRequest("CODEC", fmt.Sprintf("body unmarshal err: %s, body: %s", err.Error(), string(data)))
    }

    r.Body = io.NopCloser(bytes.NewBuffer(data))
    return nil
    }

    func ErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
    er := errors.FromError(err)

    // 获取业务错误码
    code, ok := ecode.ServiceErrorReason_value[er.Reason]
    if !ok || code == 0 { // 异常情况直接使用 errors.code
    code = er.Code
    }

    codec, contentType, ok := codecForRequest(r, "Accept")
    if !ok {
    codec, contentType, _ = codecForRequest(r, "Content-Type")
    }

    switch codec.Name() {
    case ejson.Name:
    bt, err := encodeJSONResponse(code, er.Message, []byte("{}"))
    if err != nil {
    slog.Error("fail to encode json response: %v", err)
    w.WriteHeader(stdhttp.StatusInternalServerError)
    return
    }

    w.Header().Set("Content-Type", contentType)
    w.WriteHeader(stdhttp.StatusOK)
    _, _ = w.Write(bt)
    return

    case eproto.Name:
    bt, err := encodeProtoResponse(code, er.Message, nil)
    if err != nil {
    slog.Error("fail to encode json response: %v", err)
    w.WriteHeader(stdhttp.StatusInternalServerError)
    return
    }

    w.Header().Set("Content-Type", contentType)
    w.WriteHeader(stdhttp.StatusOK)
    _, _ = w.Write(bt)
    return

    }

    return
    }

    func responseEncoder(w http.ResponseWriter, r *http.Request, i any) error {
    codec, contentType, ok := codecForRequest(r, "Accept")
    if !ok {
    codec, contentType, _ = codecForRequest(r, "Content-Type")
    }

    m, ok := i.(proto.Message)
    if !ok {
    return errors.BadRequest("CODEC", fmt.Sprintf("response is not proto.Message: %s", reflect.TypeOf(i)))
    }

    switch codec.Name() {
    case ejson.Name:
    data, err := jsonMarshalOptions.Marshal(m)
    if err != nil {
    return err
    }

    bt, err := encodeJSONResponse(200, "success", data)
    if err != nil {
    return err
    }

    w.Header().Set("Content-Type", contentType)
    w.WriteHeader(stdhttp.StatusOK)
    _, _ = w.Write(bt)
    return nil

    case eproto.Name:
    bt, err := encodeProtoResponse(200, "success", m)
    if err != nil {
    return err
    }

    w.Header().Set("Content-Type", contentType)
    w.WriteHeader(stdhttp.StatusOK)
    _, _ = w.Write(bt)
    return nil

    }
    return nil
    }

    // get codec for request
    func codecForRequest(r *http.Request, name string) (encoding.Codec, string, bool) {
    contentType := r.Header.Get(name)
    right := strings.Index(contentType, ";")
    if right == -1 {
    right = len(contentType)
    }

    c := contentType[:right]
    codec := registeredCodecs[c]
    if codec != nil {
    return codec, c, true
    }
    return jsonCodec, "application/json", false
    }

    func encodeJSONResponse(code int32, message string, data []byte) ([]byte, error) {
    buf := new(bytes.Buffer)
    buf.WriteString("{\"code\":")
    buf.WriteString(strconv.FormatInt(int64(code), 10))
    buf.WriteString(",\"message\":\"")
    buf.WriteString(message)
    buf.WriteString("\",\"ts\":" + strconv.FormatInt(time.Now().Unix(), 10) + ",")
    buf.WriteString("\"data\":")
    buf.Write(data)
    buf.WriteString("}")
    return buf.Bytes(), nil
    }

    func encodeProtoResponse(code int32, message string, data proto.Message) ([]byte, error) {
    build, err := getProtoBuilder(data)
    if err != nil {
    return nil, err
    }

    response := dynamic.NewMessage(build)
    response.SetFieldByNumber(1, code)
    response.SetFieldByNumber(2, message)
    response.SetFieldByNumber(3, int32(time.Now().Unix()))
    if data != nil {
    _ = response.TrySetFieldByNumber(4, data)
    }
    return response.Marshal()
    }

    func getProtoBuilder(message proto.Message) (*desc.MessageDescriptor, error) {
    if message == nil {
    return defaultErrorMessageDescriptor, nil
    }

    key := message.ProtoReflect().Type().Descriptor().Name()
    v, ok := messagePool.Load(key)
    if !ok || v == nil {
    anyDesc, err := desc.LoadMessageDescriptorForMessage(pproto.MessageV1(message))
    if err != nil {
    return nil, fmt.Errorf("loadMessageDescriptorForMessage err: %w", err)
    }
    build, err := builder.NewMessage("Response").
    AddField(builder.NewField("code", builder.FieldTypeInt32()).SetNumber(1)).
    AddField(builder.NewField("message", builder.FieldTypeString()).SetNumber(2)).
    AddField(builder.NewField("ts", builder.FieldTypeInt64()).SetNumber(3)).
    AddField(builder.NewField("data", builder.FieldTypeImportedMessage(anyDesc)).SetNumber(4)).Build()
    if err != nil {
    return nil, fmt.Errorf("build new message err: %w", err)
    }

    messagePool.Store(key, build)
    return build, nil
    }
    return v.(*desc.MessageDescriptor), nil
    }

    ```
    namonai
        57
    namonai  
       5 小时 43 分钟前
    > @lesismal #24 好吧,我表达的有问题,不是说随便接受 pr 。开源项目的一大优势不就是可以倾听社区的声音吗?我意思是没必要这样毫无余地的就拒绝,如果社区呼声很高,也是可以考虑的吧。

    火箭工程师不会回复「应该用无烟煤作为燃料」的观点。自然他们也不会理会你的想法。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3187 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 35ms · UTC 12:01 · PVG 20:01 · LAX 05:01 · JFK 08:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.