Go Protobuf APIv2 动态反射Protobuf使用指南

您所在的位置:网站首页 闭口粉刺药物有哪些药 Go Protobuf APIv2 动态反射Protobuf使用指南

Go Protobuf APIv2 动态反射Protobuf使用指南

#Go Protobuf APIv2 动态反射Protobuf使用指南| 来源: 网络整理| 查看: 265

今年3月,官方Go Protobuf库发布了新版API,见官方博客A new Go API for Protocol Buffers。但依然槽点颇多,首先就是版本号——

谷歌:虽然爷强制你们都按照go mod规范用语义版本(semantic version),按理来说应该在同一仓库下用v2目录区分版本,但爷自己就是不这么干,与之前的 github.com/golang/protobuf 不同,爷就是要把新的代码放在放在 github.com/protocolbuffers/protobuf-go ,然后不升大版本号,而是在之前v1版API库的版本号基础上给中版本号加16,到1.20.x,都给爷吃屎吧开发者们!! 注:此处为意译,但上面那个官方的公告文章基本上就是这么说的,不信自己去看原文

其次就是这个新库子一点像样的文档都没有,初来乍到根本不知如何使用,尤其是当你想用动态反射protobuf的方式来使用时。感觉好多go的库都是这个样子,以为有了godoc这种文档工具,在代码里写一大堆注释,别人就能知道怎么用了,结果用户看了一头雾水,最后还是得啃源码,本文就是啃了好久源码踩了无数的坑之后得来的,希望您能因为本文少走一些弯路。

看官方的意思是v1版的库(从1.4.0开始)底层也会改为基于v2(禁止套娃)并且没有要和gogo/protobuf合并的迹象,所以目前我的建议是如果你用的是原版或者魔改版的gogo/protobuf,最好不要闲着没事升级这个(又不是不能用.jpg),但如果你像我一样需要动态生成pb并且反射解析的话,那么接下来的内容就可以作为参考了。

动态protobuf的原理

这里的动态并不是指字段可以xjb变,而是在运行时根据代码逻辑构建出FieldDescriptorProto,并以此为字段的定义依据通过反射来序列化/反序列化protobuf消息。

静态pb 动态pb 字段定义 通过.proto文件定义 在运行时通过代码逻辑定义,产物是FileDescriptor 编译 通过protoc进行编译,生成固定struct和Marshal,Unmarshal等操作方法的go代码 无需编译 使用 调用生成的代码对消息进行序列化和反序列化等操作 使用protoreflect下面的反射方法,依据FileDescriptor的定义,对消息进行序列化和反序列化等操作

我这边的场景基本上是这样玩的:某个服务,启动时构建FileDescriptor,从里面掏一个MessageDescriptor出来,用它创建一个Message对象,并将数据塞进去,最后Marshal成二进制(也称为wire format)。另一个服务,启动时使用同样的过程构建FileDescriptor,从里面把MessageDescriptor掏出来,用它创建一个Message对象,把之前的那坨二进制Unmarshal进去。然后按照MessageDescriptor里面的各种FieldDescriptor(也就是字段定义)用Message对象上的一些反射方法把字段的数据取出来。(被一大堆descriptor绕晕的先别着急关闭窗口,后面会讲怎么写的)

你们golang没有泛型是真的辣鸡

准备

首先自己去go get这个版本号非常神奇的的库

go get google.golang.org/[email protected]

然后记得import,下文基本上用了这四个包,不要问我为什么protoreflect为什么缩写成pref,我从他们官方代码里抄的

import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protodesc" pref "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/descriptorpb" ) 如何定义

下面的代码定义了三个message,其中Foo是简单的无嵌套消息;Bar是消息内嵌套一个Map字段,key是string,value是前面定义的那个Foo;Baz是消息内嵌套repeated也就是列表字段,其中元素的类型是Foo。

先定义FileDescriptorProto,在里面塞MessageDescriptorProto,最后记得用protodesc.NewFile(pb, nil)来通过那个FileDescriptor生成可用的FileDescriptor,这个东西才是最终我们需要的。

简单消息定义 嵌套Map消息定义

其实是使用repeated里面塞内嵌k/v的message来表示map的,但在通过代码动态创建descriptor的时候,就要完全符合它的要求才可以,不然在后续的使用中会报错。 什么样的字段会被认为是个map呢?主要需要注意以下几点(感兴趣的请去围观该库的checkValidMap方法源代码):

Label为Repeated Type为MessageKind TypeName字段需要设置为.包名.消息名.entryDescriptor名,例如.example.Bar.BarMapEntry,这个玩意需要和NestedType里面相一致 NestedType字段里需要创建一个entry的DescriptorProto 这个东西的Name必须是你map字段名改成驼峰后面解Entry,也就是说如果你的map字段是what_the_fuck,那么这里的Name必须设置为WhatTheFuckEntry, 他的NestedType的Field里有且仅有两个,分别是key和value,而且顺序一定要是先key后value,之后key和value的的Type什么的根据你的需要进行设置 它的Options字段里面要设置MapEntry: proto.Bool(true) 嵌套List消息定义

嵌套List定义很简单,Label为Repeated,Type根据需要进行设置。

代码 // make FileDescriptorProto pb := &descriptorpb.FileDescriptorProto{ Syntax: proto.String("proto3"), Name: proto.String("example.proto"), Package: proto.String("example"), MessageType: []*descriptorpb.DescriptorProto{ // define Foo message &descriptorpb.DescriptorProto{ Name: proto.String("Foo"), Field: []*descriptorpb.FieldDescriptorProto{ { Name: proto.String("id"), JsonName: proto.String("id"), Number: proto.Int32(1), Type: descriptorpb.FieldDescriptorProto_Type(pref.Int32Kind).Enum(), }, { Name: proto.String("title"), JsonName: proto.String("title"), Number: proto.Int32(2), Type: descriptorpb.FieldDescriptorProto_Type(pref.StringKind).Enum(), }, }, }, // define Bar message &descriptorpb.DescriptorProto{ Name: proto.String("Bar"), Field: []*descriptorpb.FieldDescriptorProto{ { Name: proto.String("bar_map"), JsonName: proto.String("bar_map"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_Label(pref.Repeated).Enum(), Type: descriptorpb.FieldDescriptorProto_Type(pref.MessageKind).Enum(), TypeName: proto.String(".example.Bar.BarMapEntry"), }, }, NestedType: []*descriptorpb.DescriptorProto{ { Name: proto.String("BarMapEntry"), Field: []*descriptorpb.FieldDescriptorProto{ { Name: proto.String("key"), JsonName: proto.String("key"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_Label(pref.Optional).Enum(), Type: descriptorpb.FieldDescriptorProto_Type(pref.StringKind).Enum(), }, { Name: proto.String("value"), JsonName: proto.String("value"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_Label(pref.Optional).Enum(), Type: descriptorpb.FieldDescriptorProto_Type(pref.MessageKind).Enum(), TypeName: proto.String(".example.Foo"), }, }, Options: &descriptorpb.MessageOptions{ MapEntry: proto.Bool(true), }, }, }, }, // define Baz message &descriptorpb.DescriptorProto{ Name: proto.String("Baz"), Field: []*descriptorpb.FieldDescriptorProto{ { Name: proto.String("baz_list"), JsonName: proto.String("baz_list"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_Label(pref.Repeated).Enum(), Type: descriptorpb.FieldDescriptorProto_Type(pref.MessageKind).Enum(), TypeName: proto.String(".example.Foo"), }, }, }, }, } // get FileDescriptor fd, err := protodesc.NewFile(pb, nil)

上面的定义基本等效于这个proto文件,实际的逻辑中不会用到这个文件,仅用作参考,理论上来说用它编译出的go代码也可以正常用来操作动态定义出来的pb生成的数据。

syntax = "proto3"; package myproto; message Foo { int id = 1; string title = 2; string content = 3; } message Bar { Map bar_map = 1; } message Bazz { repeated int my_list = 1; } 如何创建消息及序列化 简单消息

往message里塞数据的套路基本都是一样的,先获取要修改的字段的FieldDescriptor,这个东西可以通过abcMessageDescriptor.Fields().ByName("field_name")获取,或者也有ByNumber方法等传入字段序号获取,用法类似。取到FieldDescriptor之后就可以用Set方法把值设置上去了。

var ( msg *dynamicpb.Message data []byte ) fooMessageDescriptor := fd.Messages().ByName("Foo") msg := dynamicpb.NewMessage(fooMessageDescriptor) msg.Set(fooMessageDescriptor.Fields().ByName("id"), pref.ValueOfInt32(42)) msg.Set(fooMessageDescriptor.Fields().ByNumber(1), pref.ValueOfString("aloha")) 嵌套Map

先取到Map字段的FieldDescriptor,然后传入到NewField方法,获取Map字段,使用Set往里面塞数据,最后别忘了把Map通过Set方法写入到msg中,如果你要写入的Value是可变的,要用Mutable方法进行操作。

barMessageDescriptor := fd.Messages().ByName("Bar") msg := dynamicpb.NewMessage(barMessageDescriptor) mf := barMessageDescriptor.Fields().ByName("bar_map") mp:= msg.NewField(mf) fooMsg := makeFooMsg(fd) mp.Map().Set(pref.MapKey(pref.ValueOfString("key1")), pref.ValueOfMessage(fooMsg)) mp.Map().Set(pref.MapKey(pref.ValueOfString("key2")), pref.ValueOfMessage(fooMsg)) msg.Set(mf, mp) 嵌套List(即repeated)

先取到List字段的FieldDescriptor,然后传入到NewField方法,获取List,使用Append往里面塞元素,最后别忘了把List通过Set方法写入到msg中,如果你要写入的Value是可变的,要用MutableAppend方法操作。

bazMessageDescriptor := fd.Messages().ByName("Baz") msg := dynamicpb.NewMessage(bazMessageDescriptor) lf := bazMessageDescriptor.Fields().ByName("baz_list") fooMsg := makeFooMsg(fd) lst := msg.NewField(lf).List() lst.Append(pref.ValueOf(fooMsg)) lst.Append(pref.ValueOf(fooMsg)) lst.Append(pref.ValueOf(fooMsg)) msg.Set(lf, pref.ValueOf(lst)) 如何反序列化消息并获取字段数据 反序列化

先用dynamicpb.NewMessage传入需要的MessageDescriptor新建Message对象,再调用proto.Unmarshal方法,把数据解到msg里面。

var ( data []byte err error ) barMessageDescriptor := fd.Messages().ByName("Bar") msg := dynamicpb.NewMessage(barMessageDescriptor) if err := proto.Unmarshal(data, msg); err != nil { panic(err) } 获取普通字段数据

用Message上的Get方法传需要的字段的descriptor进去,就可以取到值,用Range方法可以传入一个函数对各个字段进行遍历,return false的时候可以直接跳出循环。

fooMessageDescriptor := fd.Messages().ByName("Foo") msg := dynamicpb.NewMessage(fooMessageDescriptor) if err := proto.Unmarshal(data, msg); err != nil { panic(err) } // get single field's value v := msg.Get(fooMessageDescriptor.Fields().ByName("id")) fmt.Printf("get %v \n", v) // iterate over all fields msg.Range(func(descriptor pref.FieldDescriptor, value pref.Value) bool { fmt.Printf("field: %v value: %v \n", descriptor.Name(), value) return true }) 获取嵌套Map数据

用Message上的Get方法传Map字段的descriptor进去,return false的时候可以直接跳出循环。

barMessageDescriptor := fd.Messages().ByName("Bar") msg := dynamicpb.NewMessage(barMessageDescriptor) if err := proto.Unmarshal(data, msg); err != nil { panic(err) } mp := msg.Get(barMessageDescriptor.Fields().ByName("bar_map")).Map() // iterate over map field mp.Range(func(key pref.MapKey, value pref.Value) bool { fmt.Printf("key: %v value: %v \n", key.String(), value.Message()) return true }) 获取List(即repeated)数据

用Message上的Get方法传List字段的descriptor进去,然后先用Len获取长度,再用Get方法传入index获取各个元素。

bazMessageDescriptor := fd.Messages().ByName("Baz") msg := dynamicpb.NewMessage(bazMessageDescriptor) if err := proto.Unmarshal(data, msg); err != nil { panic(err) } lf := bazMessageDescriptor.Fields().ByName("baz_list") lst := msg.Get(lf).List() length := lst.Len() for i:= 0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3