前面讲过protobuf工具protoc的使用,本章我们将简单介绍一下protobuf的语法和相关细节。
protobuf实际上是一套类似于Json或者XML的数据传输格式和规范,用于不同应用、平台之间的通信,而message就是作为protobuf中定义通信的数据格式。如下,我们可以定义一个message结构:
message Foo {}
在protoc-gen-go插件的作用下,会生成一个名为Foo的结构体,且*Foo会实现proto.Message接口,生成的pg.go文件如下:
type Foo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *Foo) Reset() {
*x = Foo{}
if protoimpl.UnsafeEnabled {
mi := &file_simplepb_simple_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Foo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Foo) ProtoMessage() {}
func (x *Foo) ProtoReflect() protoreflect.Message {
mi := &file_simplepb_simple_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
其中,Reset()、String()和ProtoMessage()都是MessageV1定义的接口,在我们的版本中并用不到,为了兼容,生成器还是生成了这部分接口实现。
type MessageV1 interface {
Reset()
String() string
ProtoMessage()
}
而实际中真正用到的是MessageV2,这个接口定义了一个ProtoReflect()方法,此方法返回一个名为protoreflect.Message的接口,这个接口提供基于反射的message接口定义。
上面简单介绍了结构体的生成,接下来介绍一下结构体中的字段。比如人员描述的相关对象如下:
message Profile {
string name = 1;
int32 age = 2;
}
2.1 字段编号
message中的每一个字段都有一个唯一的编号,编号的范围从1到229-1,这些编号用于在编码后的二进制中标识字段,对于这些编号,特别是对于与外部平台或者设备交互的接口,一旦确定就不能更改,否则可能造成不同版本之间的不兼容。
需要注意的是,当字段编号在1-15时,在protobuf的编码规则下,其只需一个byte即可编码编号和类型;而当字段编号在16-2047时,就需要两个byte了。所以针对高并发、频繁调用的数据接口,最好使用字段1-15。
另外,我们不能使用数字19000-19999作为字段编号,这是为protobuf保留的。
2.2 字段规则
字段的规则可以有以下几种:
如下,我们设置以下的字段规则;其生成的pb.go中结构体也不尽相同。
处。
message Profile {
string name = 1;
optional int32 age = 2;
repeated string hobbies = 3;
map<int32,int32> calender = 4;
}
生成的pb.go文件如下:
type Profile struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Age *int32 `protobuf:"varint,2,opt,name=age,proto3,oneof" json:"age,omitempty"`
Hobbies []string `protobuf:"bytes,3,rep,name=hobbies,proto3" json:"hobbies,omitempty"`
Calender map[int32]int32 `protobuf:"bytes,4,rep,name=calender,proto3" json:"calender,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}
2.3 保留字段
如果我们更改了接口,删除了一些不再使用的字段,那么后来者可能会使用这些字段表示另外一些含义,对于一些对外的接口,可能引起版本之间的不兼容,可能会引起数据损坏、隐私暴露等严重错误,所以我们使用reserve字段来保留这些字段,防止错误的发生,如下:
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
2.4 字段类型
字段的类型众多,和各大语言之间的对应关系如:Scalar Value Types
2.5 字段默认值
对于编码后的数据而言,如果singular字段没有被设置值,那么相应字段会被设置为该字段的默认值。不同类型的字段有着不同的默认值:
需要注意的是,当接收方拿到零值类型对象的时候,无法判断这个对象是由对方设置了零值,还是没有设置任何值,所以,不要用零值去触发一些特定的行为,可能会发生不可预计的事情。在go中,如果需要区分设置了零值和没有设置任何值,可以用optional规则。
另外,零值是不会被序列化的。
message Profile {
string name = 1;
optional int32 age = 2;
repeated string hobbies = 3;
map<int32,int32> calender = 4;
enum Gender {
UNKNOWN = 0;
MALE = 1;
FEMALE = 2;
}
Gender gender = 5;
}
如上,gender字段就是枚举类型,其有以下两个特点:
可以使用option allow_alias = true使用枚举别名,如下,这时候,MAN和MALE就互为别名。
enum Gender {
option allow_alias = true;
UNKNOWN = 0;
MAN = 1;
MALE = 1;
WOMAN = 2;
FEMALE = 2;
}
值得注意的是,枚举常量是32位的int常量,因为其使用变长编码,所以使用负数作为枚举值的序列化效率是很低的。
protobuf的map类型定义如下,其中,key_type可以是字符串或者整型,value_type可以是除了另一个map外的所有类型。
map<key_type, value_type> map_field = N;
如果对于消息中,有多个字段,同时至多只能选择一个字段的,可以使用oneof类型,oneof类型的字段中所有字段共享内存,如果一个字段被设置,那么其他字段自动被删除。如果设置了多个值,那么最后一个设置的值将覆盖前面的值。
如下,我们可以设置Profile的头像Avatar如下,其头像可以是图片存储的url,也可以直接是图片数据,这里我们就可以使用oneof类型。
package account;
message Profile {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
map<int32,int32> calender = 4;
enum Gender {
option allow_alias = true;
UNKNOWN = 0;
MAN = 1;
MALE = 1;
WOMAN = 2;
FEMALE = 2;
}
Gender gender = 5;
oneof avatar {
string image_url = 6;
bytes image_data = 7;
}
}
设置值的时候可以这样写:
p1 := &account.Profile{
Avatar: &account.Profile_ImageUrl{"http://example.com/image.png"},
}
// imageData is []byte
imageData := getImageData()
p2 := &account.Profile{
Avatar: &account.Profile_ImageData{imageData},
}
解析字段的时候可以这样写:
switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
// Load profile image based on URL
// using x.ImageUrl
case *account.Profile_ImageData:
// Load profile image based on bytes
// using x.ImageData
case nil:
// The field is not set.
default:
return fmt.Errorf("Profile.Avatar has unexpected type %T", x)
}
Any类型允许我们使用此字段传递任何一个protobuf类型的消息,类似于某些语言中泛型,如下,其只有两个字段,value字段是序列化后的二进制值,type_url则是该类型的唯一标识符,在golang中,所有message的唯一标识符都可以通过反射m.ProtoReflect().Descriptor().FullName()拿到。
为了使用any类型,我们需要import “google/protobuf/any.proto”。proto文件的下载地址为链接。
any
message Any {
// ...
string type_url = 1;
// Must be a valid serialized protocol buffer of the above specified type.
bytes value = 2;
}
golang库的底层提供了一些函数用于发送或者接收的函数,譬如,发送时,我们可以使用以下方式序列化我们希望序列化的消息:
detail, err := anypb.New(&AnyA{})
解析接收数据时,我们可以用以下方法判断类型或者反序列化消息:
// MessageIs 判断是否为此message类型
m.Detail.MessageIs((*AnyA)(nil))
// UnmarshalTo 将any解析到具体类型
var a AnyA
err := m.Detail.UnmarshalTo(&a)
// UnmarshalNew 将消息类型转回动态类型
dy, err := m.Detail.UnmarshalNew()
_, ok := dy.(*AnyA)
struct类型和any类型是类似的,都是protobuf提供的不透明配置嵌入的方式。不同的是,any是将带有类型信息的二进制序列化的protobuf嵌入到另一个protobuf的字段内;而struct是最外层不能是数组的动态json类型。
message Struct {
// Unordered map of dynamically typed values.
map<string, Value> fields = 1;
}
// JSON 数据类型
message Value {
// The kind of value.
oneof kind {
// Represents a null value.
NullValue null_value = 1;
// Represents a double value.
double number_value = 2;
// Represents a string value.
string string_value = 3;
// Represents a boolean value.
bool bool_value = 4;
// Represents a structured value.
Struct struct_value = 5;
// Represents a repeated `Value`.
ListValue list_value = 6;
}
}
// JSON null
enum NullValue {
// Null value.
NULL_VALUE = 0;
}
// JSON 数组
message ListValue {
repeated Value values = 1;
}
同样的,为了使用struct类型,我们需要import “google/protobuf/struct.proto”。proto文件的下载地址为链接。
我们也可以使用以下的方法去创建struct类型:
// NewStruct 从map表创建struct
s, err := structpb.NewStruct(m)
// UnmarshalJSON 从json串生成strcut
s := &_struct.Struct{}
marshal, _ := json.Marshal(data)
err := s.UnmarshalJSON(marshal)
可以使用以下的方法去解析接收到的strcut类型数据:
// AsMap 将strcut转换成map
m := s.AsMap()
// MarshalJSON 将struct转换为[]byte
data, err := s.MarshalJSON()
_ = json.Unmarshal(data, v)