protobuf
https://geektutu.com/post/quick-go-protobuf.html
https://colobu.com/2019/10/03/protobuf-ultimate-tutorial-in-go/
https://github.com/protocolbuffers/protobuf
protobuf 即 Protocol Buffers,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。
protobuf 性能和效率大幅度优于 JSON、XML 等其他的结构化数据格式。
protobuf 是以二进制方式存储的,占用空间小,但也带来了可读性差的缺点。
protobuf 在通信协议和数据存储等领域应用广泛。
例如著名的分布式缓存工具 Memcached 的 Go 语言版本 groupcache 就使用了 protobuf 作为其 RPC 数据格式。
安装
| $ protoc --version
libprotoc 3.8.0
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
$ which protoc-gen-go
/Users/nocilantro/go/bin/protoc-gen-go
|
定义消息类型
| $ mkdir student
$ cd student
$ go mod init example.com/student
|
新建 pb/student.proto
:
1
2
3
4
5
6
7
8
9
10
11
12 | syntax = "proto3";
option go_package = "example.com/student/pb";
package student; // 包名声明符是可选的,用来防止不同的消息类型有命名冲突。
// this is a comment
// 每个字符 = 后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变,标识符的取值范围为 [1, 2^29 - 1] 。
message Student { // 消息类型 使用 message 关键字定义,Student 是类型名,name, male, scores 是该类型的 3 个字段,类型分别为 string, bool 和 []int32。字段可以是标量类型,也可以是合成类型。
string name = 1;
bool male = 2;
repeated int32 scores = 3; // 每个字段的修饰符默认是 singular,一般省略不写,repeated 表示字段可重复,即用来表示 Go 语言中的数组类型。
}
|
| $ protoc --go_out=. --go_opt=paths=source_relative pb/student.proto
$ ls pb
student.pb.go student.proto
|
可以看到该目录下多出了一个 Go 文件 student.pb.go。这个文件内部定义了一个结构体 Student,以及相关的方法:
pb/student.pb.go
:
| ...
type Student struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Male bool `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
Scores []int32 `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
}
...
|
以下是一个非常简单的例子,即证明被序列化的和反序列化后的实例,包含相同的数据。
main.go
:
| $ go get github.com/golang/protobuf/proto
$ go get google.golang.org/protobuf/reflect/protoreflect
$ go get google.golang.org/protobuf/runtime/protoimpl
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | package main
import (
"log"
"example.com/student/pb"
"google.golang.org/protobuf/proto"
)
func main() {
test := &pb.Student{
Name: "nocilantro",
Male: true,
Scores: []int32{98, 85, 88},
}
data, err := proto.Marshal(test)
if err != nil {
log.Fatal("marshaling error: ", err)
}
newTest := &pb.Student{}
err = proto.Unmarshal(data, newTest)
if err != nil {
log.Fatal("unmarshaling error: ", err)
}
// Now test and newTest contain the same data.
if test.GetName() != newTest.GetName() {
log.Fatalf("data mismatch %q != %q", test.GetName(), newTest.GetName())
}
}
|
future feature
https://github.com/grpc/grpc-go/tree/master/cmd/protoc-gen-go-grpc
By default, to register services using the methods generated by this tool, the service implementations must embed the corresponding UnimplementedServer for future compatibility. This is a behavior change from the grpc code generator previously included with protoc-gen-go. To restore this behavior, set the option require_unimplemented_servers=false. E.g.:
| protoc --go-grpc_out=require_unimplemented_servers=false[,other options...]:. \
|
字段类型
标量类型
proto类型 |
go类型 |
备注 |
double |
float64 |
|
float |
float32 |
|
int32 |
int32 |
|
uint32 |
uint32 |
|
sint32 |
uint32 |
适合负数 |
int64 |
int64 |
|
uint64 |
uint64 |
|
sint64 |
int64 |
适合负数 |
fixed32 |
uint32 |
固长编码,适合大于 2^28 的值 |
sfixed32 |
int32 |
固长编码 |
fixed64 |
uint64 |
固长编码,适合大于 2^56 的值 |
sfixed64 |
int64 |
固长编码 |
bool |
bool |
|
string |
string |
UTF8 编码,长度不超过 2^32 |
bytes |
[]byte |
任意字节序列,长度不超过 2^32 |
标量类型如果没有被赋值,则不会被序列化,解析时,会赋予默认值。
- string: 空字符串
- bytes: 空序列
- bool: false
- 数值类型: 0
枚举(enumerations)
| message Student {
string name = 1;
enum Gender {
FEMALE = 0;
MALE = 1;
}
Gender gender = 2;
repeated int32 scores = 3;
}
|
生成代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | ...
type Student_Gender int32
const (
Student_FEMALE Student_Gender = 0
Student_MALE Student_Gender = 1
)
// Enum value maps for Student_Gender.
var (
Student_Gender_name = map[int32]string{
0: "FEMALE",
1: "MALE",
}
Student_Gender_value = map[string]int32{
"FEMALE": 0,
"MALE": 1,
}
)
...
type Student struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Gender Student_Gender `protobuf:"varint,2,opt,name=gender,proto3,enum=student.Student_Gender" json:"gender,omitempty"`
Scores []int32 `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
}
|
- 枚举类型的第一个选项的标识符必须是0,这也是枚举类型的默认值。
- 别名(Alias),允许为不同的枚举值赋予相同的标识符,称之为别名,需要打开 allow_alias 选项。
| message EnumAllowAlias {
enum Status {
option allow_alias = true;
UNKOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
|
生成代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | type EnumAllowAlias_Status int32
const (
EnumAllowAlias_UNKOWN EnumAllowAlias_Status = 0
EnumAllowAlias_STARTED EnumAllowAlias_Status = 1
EnumAllowAlias_RUNNING EnumAllowAlias_Status = 1
)
// Enum value maps for EnumAllowAlias_Status.
var (
EnumAllowAlias_Status_name = map[int32]string{
0: "UNKOWN",
1: "STARTED",
// Duplicate value: 1: "RUNNING",
}
EnumAllowAlias_Status_value = map[string]int32{
"UNKOWN": 0,
"STARTED": 1,
"RUNNING": 1,
}
)
...
type EnumAllowAlias struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
|
使用定义好的其他消息类型
Result
是另一个自定义的消息类型,在 SearchReponse
作为一个消息字段类型使用。
| message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
message SearchResponse {
repeated Result results = 1;
}
|
生成的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | type Result struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
Snippets []string `protobuf:"bytes,3,rep,name=snippets,proto3" json:"snippets,omitempty"`
}
...
type SearchResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Results []*Result `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"`
}
|
嵌套写也是支持的:
| message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
|
生成的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | type SearchResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Results []*SearchResponse_Result `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"`
}
...
type SearchResponse_Result struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
Snippets []string `protobuf:"bytes,3,rep,name=snippets,proto3" json:"snippets,omitempty"`
}
|
如果定义在其他文件中,可以导入其他消息类型来使用:
| import "myproject/other_protos.proto";
|
任意类型(Any)
Any 可以表示不在 .proto
中定义任意的内置类型。
| import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
|
生成代码:
1
2
3
4
5
6
7
8
9
10
11
12
13 | import (
any "github.com/golang/protobuf/ptypes/any"
...
)
...
type ErrorStatus struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
Details []*any.Any `protobuf:"bytes,2,rep,name=details,proto3" json:"details,omitempty"`
}
|
oneof
如果你有一组字段,同时最多允许这一组中的一个字段出现,就可以使用 oneof定义这一组字段,这有点 Union 的意思,但是 Oneof 允许你设置零各值。
因为 proto3 没有办法区分正常的值是否是设置了还是取得缺省值(比如int64类型字段,如果它的值是0,你无法判断数据是否包含这个字段,因为0几可能是数据中设置的值,也可能是这个字段的零值),所以你可以通过Oneof取得这个功能,因为Oneof有判断字段是否设置的功能。
| message OneofMessage {
oneof test_oneof {
string name = 4;
int64 value = 9;
}
}
|
生成代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | type OneofMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to TestOneof:
// *OneofMessage_Name
// *OneofMessage_Value
TestOneof isOneofMessage_TestOneof `protobuf_oneof:"test_oneof"`
}
...
type isOneofMessage_TestOneof interface {
isOneofMessage_TestOneof()
}
type OneofMessage_Name struct {
Name string `protobuf:"bytes,4,opt,name=name,proto3,oneof"`
}
type OneofMessage_Value struct {
Value int64 `protobuf:"varint,9,opt,name=value,proto3,oneof"`
}
func (*OneofMessage_Name) isOneofMessage_TestOneof() {}
func (*OneofMessage_Value) isOneofMessage_TestOneof() {}
|
oneof
字段不能同时使用 repeated
。
map类型
map类型需要设置键和值的类型,格式是 "map" "<" keyType "," type ">" mapName "=" fieldNumber [ "[" fieldOptions "]"
。
| message MapRequest {
map<string, int32> points = 1;
}
|
生成代码:
| type MapRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Points map[string]int32 `protobuf:"bytes,1,rep,name=points,proto3" json:"points,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}
|
reserved
reserved 可以用来指明此 message 不使用某些字段,也就是忽略这些字段。
可以通过字段编号范围或者字段名称指定保留的字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | message AllNormalypes {
reserved 2, 4 to 6;
reserved "field14", "field11";
double field1 = 1;
// float field2 = 2;
int32 field3 = 3;
// int64 field4 = 4;
// uint32 field5 = 5;
// uint64 field6 = 6;
sint32 field7 = 7;
sint64 field8 = 8;
fixed32 field9 = 9;
fixed64 field10 = 10;
// sfixed32 field11 = 11;
sfixed64 field12 = 12;
bool field13 = 13;
// string field14 = 14;
bytes field15 = 15;
string testfield = 16;
}
|
生成代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | type AllNormalypes struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Field1 float64 `protobuf:"fixed64,1,opt,name=field1,proto3" json:"field1,omitempty"`
// float field2 = 2;
Field3 int32 `protobuf:"varint,3,opt,name=field3,proto3" json:"field3,omitempty"`
// int64 field4 = 4;
// uint32 field5 = 5;
// uint64 field6 = 6;
Field7 int32 `protobuf:"zigzag32,7,opt,name=field7,proto3" json:"field7,omitempty"`
Field8 int64 `protobuf:"zigzag64,8,opt,name=field8,proto3" json:"field8,omitempty"`
Field9 uint32 `protobuf:"fixed32,9,opt,name=field9,proto3" json:"field9,omitempty"`
Field10 uint64 `protobuf:"fixed64,10,opt,name=field10,proto3" json:"field10,omitempty"`
// sfixed32 field11 = 11;
Field12 int64 `protobuf:"fixed64,12,opt,name=field12,proto3" json:"field12,omitempty"`
Field13 bool `protobuf:"varint,13,opt,name=field13,proto3" json:"field13,omitempty"`
// string field14 = 14;
Field15 []byte `protobuf:"bytes,15,opt,name=field15,proto3" json:"field15,omitempty"`
Testfield string `protobuf:"bytes,16,opt,name=testfield,proto3" json:"testfield,omitempty"`
}
|
更新消息类型
有时候你不得不修改正在使用的proto文件,比如为类型增加一个字段,protobuf支持这种修改而不影响已有的服务,不过你需要遵循一定的规则:
- 不要改变已有字段的字段编号
- 当你增加一个新的字段的时候,老系统序列化后的数据依然可以被你的新的格式所解析,只不过你需要处理新加字段的缺省值。 老系统也能解析你信息的值,新加字段只不过被丢弃了
- 字段也可以被移除,但是建议你 Reserved 这个字段,避免将来会使用这个字段
- int32, uint32, int64, uint64 和 bool 类型都是兼容的
- sint32 和 sint64 兼容,但是不和其它整数类型兼容
- string 和 bytes 兼容,如果 bytes 是合法的 UTF-8 bytes的话
- 嵌入类型和 bytes 兼容,如果 bytes 包含一个消息的编码版本的话
- fixed32和sfixed32, fixed64和sfixed64 兼容
- enum和int32, uint32, int64, uint64格式兼容
- 把单一一个值改变成一个新的oneof类型的一个成员是安全和二进制兼容的。把一组字段变成一个新的oneof字段也是安全的,如果你确保这一组字段最多只会设置一个。把一个字段移动到一个已存在的oneof字段是不安全的
定义服务
如果消息类型是用来远程通信的(Remote Procedure Call, RPC),可以在 .proto
文件中定义 RPC 服务接口。例如我们定义了一个名为 SearchService 的 RPC 服务,提供了 Search 接口,入参是 SearchRequest 类型,返回类型是 SearchResponse
| service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
|
推荐风格
文件(Files)
- 文件名使用小写下划线的命名风格,例如 lower_snake_case.proto
- 每行不超过 80 字符
- 使用 2 个空格缩进
包(Packages)
- 包名应该和目录结构对应,例如文件在
my/package/
目录下,包名应为 my.package
消息和字段(Messages & Fields
)
- 消息名使用首字母大写驼峰风格(CamelCase),例如
message StudentRequest { ... }
- 字段名使用小写下划线的风格,例如
string status_code = 1
- 枚举类型,枚举名使用首字母大写驼峰风格,例如
enum FooBar
,枚举值使用全大写下划线隔开的风格(CAPITALS_WITH_UNDERSCORES
),例如 FOO_DEFAULT=1
服务(Services)
- RPC 服务名和方法名,均使用首字母大写驼峰风格,例如
service FooService{ rpc GetSomething() }
message 中的选项
allow_alias
:
别名(Alias),允许为不同的枚举值赋予相同的标识符,称之为别名,需要打开 allow_alias 选项。
| message EnumAllowAlias {
enum Status {
option allow_alias = true;
UNKOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
|
Method Options
https://engineering.issuu.com/ocaml-protoc-plugin/ocaml-protoc-plugin/Descriptor/Google/Protobuf/MethodOptions/index.html
eg.
| rpc Test(TestRequest) returns (TestResponse) {
option idempotency_level = IDEMPOTENCY_UNKNOWN;
option (google.api.http) = {
post: "/v1/test"
body: "*"
};
}
|