protobuf 不支持泛型?

212 天前
 ljzxloaf

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

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

public class Response<T>  {

    @NotNull
    private ResponseStatus status;

    private T result;

 }
5398 次点击
所在节点    程序员
59 条回复
jigujigushanshan
211 天前
@5261 性能啊,解析速度快 体积小 传输速度快,相反如果加泛型 想都不用想唯一的优势都没了,那还不如 json 各种场景一把梭
hidemyself
211 天前
我理解出现这种场景,说明不适合用 pb
AEnjoyable
211 天前
看你要传输什么东西 正常的就 any + wrapper 包, 不然就像楼上说的 byte,然后自己反序列化
bli22ard
211 天前
你是先有 proto 文件,再生成数据存储类,而不是先有你发的 Response<T> 这种定义。proto 定义的可能最佳做法是,定义一个 Head message ,定义 code 和 msg ,然后 service 的方法返回值,message RespOrderList{
Head head=1;
repeated Order orderList=2;
} 。protobuf 要是加个泛型,你让没有泛型的语言怎么实现。另外,即使 java 代码中使用 json , 我也不觉得,Response<T> 是一种好的做法。
lpxxn
211 天前
最好不要用 bytes ,调用方非常难受。
rev1si0n
211 天前
这个问题,要么填 bytes 自己序列化,要么 oneof 。纠结这个说明你可能也没有深入使用也没有看看别人是怎么做的还说让人支持,你在破坏它的通用性,为了你的需求需要做两个版本的文档。建议自己搞个,最好别用,我只能说以后的坑还多了。
MEIerer
211 天前
这两天准备看这个来着
itskingname
211 天前
@debuggerx 写 Java 的人就喜欢什么都包好几层,就跟他们的空文件夹嵌套几十层一样。
securityCoding
211 天前
可能是定位有偏差,你定义一个通用 baseResult 字段就好了,用反射来解决
rev1si0n
211 天前
我觉得这个定义适合你做参考,开发的时候甚至没有那么多现成的项目可以让我参考该怎么做。https://github.com/firerpa/lamda/tree/6e1298b536d344527ddcb94e621f1b3a88aa6f32/lamda/rpc
zmcity
211 天前
因为不是所有语言都支持泛型,如果 pb 强行兼容,在 c 语言等面向过程的语言的 sdk 里使用就会变得异常抽象。
所以要么使用 oneof ,要么换一个完全放弃面向过程的语言的库,比如 microsoft.bond
ljzxloaf
211 天前
@lpxxn 用 byte,any 还不如多写点 boilerplate code
lesismal
211 天前
> @lesismal #24 好吧,我表达的有问题,不是说随便接受 pr 。开源项目的一大优势不就是可以倾听社区的声音吗?我意思是没必要这样毫无余地的就拒绝,如果社区呼声很高,也是可以考虑的吧。

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

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

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

越读书越学习越觉得自己无知,共勉!
liuidetmks
211 天前
用脚本生成? pmake ?
EvaCcino
211 天前
包一层感觉没啥意义,如果包一层都有意义的话, 为什么不直接包 1000w 层?
ryan961
210 天前
@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
210 天前
> @lesismal #24 好吧,我表达的有问题,不是说随便接受 pr 。开源项目的一大优势不就是可以倾听社区的声音吗?我意思是没必要这样毫无余地的就拒绝,如果社区呼声很高,也是可以考虑的吧。

火箭工程师不会回复「应该用无烟煤作为燃料」的观点。自然他们也不会理会你的想法。
lesismal
207 天前
@namonai #57
所以到现在你还是觉得 “也就是个 message”、“只是支持而已”。

如果自己只在第一层,看不到第五层拒绝的原因很正常、可以理解。但是二三四五层的好些人出来解释了好多次了都不看、这属于是自己强行用自己在第一层的视角去黑第五层。
那没什么可聊的了
namonai
206 天前
@lesismal 酝酿半天,发现之前的帖子回错人了……我赞成你的说法,要倾听社区正确的生意而不是盲目倾听社区的声音。我只不过是想喷楼主一顿,感觉有点傻,应用层的需求非要加到协议层,啥都没学懂。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1120994

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX