这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。
本文主要参考Protocol Buffer Basics: Go,只有一些记录+官方手册的部分翻译,不完全遵循原文顺序。
为什么使用Protocol Buffer?
Protocol Buffer可以理解一种序列化结构数据的方案,可以与围绕XML/JSON等格式订制的方案相比,它最主要的特点就是以二进制格式编码,从而实现高效的内容压缩和网络传输。下面是原文翻译:
- Protocol Buffer是解决这个问题(XML消耗大量空间,XML的编/解码严重降低性能。从DOM树中检索数据非常复杂)的灵活、高效、自动化的解决方案。
- Protocol Buffer对数据结构的定义是通过
.proto
格式的描述文件实现的。 - Protocol Buffer的编译器会创建一个通过高效的二进制格式实现自动编/解码Protocol Buffer数据的类,从而提供Protocol Buffer各字段的getter和setter方法,并处理好相关的读写细节。
- Protocol Buffer还支持随时间扩展扩展格式的特性,因此对应新格式的代码仍然可以读取用旧格式编码的数据(应该有一些限制)。
安装
-
安装Protocol Buffer的编译器protoc Protocol Buffers Github下载页面 需要把/bin文件夹添加到环境变量/系统路径。
-
安装go Protocol Buffer插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
虽然我挂了梯子,但是这个网址似乎不响应代理链接,直连又连不上,所以不得不临时配了一下goproxy。参考 goproxy.cn/ 里面的描述配置。
.proto
文件编写示例
.proto
文件文件中的定义相对比较简单,主要用到的语句是message
,用message
声明需要序列化的数据结构,并给每一个字段指定名称和类型,可以嵌套。示例文件是 addressbook.proto.
// 最新的语法格式应该是proto3
syntax = "proto3";
// .proto文件最好以package声明开头,可以避免项目间的命名冲突。
package tutorial;
// 导入依赖文件
import "google/protobuf/timestamp.proto";
// go_package定义了package的import路径,编译后的类文件会保存在当前目录下的同名文件夹(下面的字符串)。
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
// message是一组有类型字段的集合。
// bool、int32、float、double和string等类型可用作字段,也可以嵌套message。
message Person {
// 字段里的"= 1"作为二进制编码当中的唯一标签。
// 建议采用1-15作为常用或重复字段的编号,而将16及以上的编号用于可选字段。
string name = 1;
int32 id = 2; // 某人的唯一ID
string email = 3;
// 枚举类型
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
// 嵌套的message类型
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
如果没有设置字段的值将会以默认值初始化,数值类型的默认值为0,字符串默认值为空字符串,布尔值为false。嵌套的message类型默认值为"default instance" or "prototype",所有的字段都为空,访问未显示设置的字段会返回字段的默认值。
如果字段声明为repeated
类型,则该字段可以重复任意次数(包括零次)。重复值将会依序将保存在Protocol Buffer中。可以将重复字段理解为动态数组。
更详细的内容可以参考Protocol Buffer Language Guide。此外,Protocol Buffer没有类似于类继承的工具。
示例代码编译
相对简单的方式,比如假定main文件夹下存在一个addressbook.proto文件,那么在main文件夹下启动命令行,可以采用如下命令完成编译:
# 确认编译器是否配置好,并查看编译器版本
protoc --version
# 编译命令,输出文件夹为当前目录,待编译的文件为当前目录下的所有以.proto为后缀的文件
protoc --go_out=. *.proto
然后可以在之前定义的go_package github.com/protocolbuffers/protobuf/examples/go/tutorialpb
目录下得到一个类似于下列代码的文件:
// versions:
// 这篇文章发布时的最新版本
// protoc-gen-go v1.28.0
// protoc v3.20.1
// source: addressbook.proto
package tutorialpb
// 枚举类型完全是go风格的
var (
Person_PhoneType_name = map[int32]string{ 0: "MOBILE" }
Person_PhoneType_value = map[string]int32{ "MOBILE": 0 }
)
// 这里也用到了标签(tag)字符串
type Person struct {
Id int32 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` // Unique ID number for this person.
Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"`
LastUpdated *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"`
}
// getter方法,但我没找到setter?
func (x *Person) GetId() int32 {
if x != nil {
return x.Id
}
return 0
}
// 不确定是不是Protocol Buffer根据每个类生成的单独的二进制文件格式
var file_addressbook_proto_rawDesc = []byte{
0x0a, 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x6f, 0x6b, 0x2e, 0x70,
0x6f, 0x74, 0x6f, 0x12, 0x08, 0x74, 0x75, 0x74, 0x6f, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x1f
}
kitex框架使用protobuf时的小问题
kitex框架同时支持thrift和protobuf两种IDL,其中优先支持thrift。如果需要使用protobuf的话需要通过-type参数进行指定,但是在翻译实例项目的thrift文件到protobuf的时候遇到一个问题,即--experimental_allow_proto3_optional未指定,通过-protobuf参数指定无效,后来发现是proto3标准下默认所有的字段是optional的,所以没有必要再像thrift一样指定哪些字段是optional的。