Skip to content

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 数据格式。

安装

1
2
3
4
5
6
7
8
$ 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

定义消息类型

1
2
3
$ 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 语言中的数组类型。
}
1
2
3
$ 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
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:

1
2
3
$ 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.:

1
  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)

1
2
3
4
5
6
7
8
9
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 选项。
1
2
3
4
5
6
7
8
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 作为一个消息字段类型使用。

1
2
3
4
5
6
7
8
9
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"`
}

嵌套写也是支持的:

1
2
3
4
5
6
7
8
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"`
}

如果定义在其他文件中,可以导入其他消息类型来使用:

1
import "myproject/other_protos.proto";

任意类型(Any)

Any 可以表示不在 .proto 中定义任意的内置类型。

1
2
3
4
5
6
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有判断字段是否设置的功能。

1
2
3
4
5
6
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 "]"

1
2
3
message MapRequest {
    map<string, int32> points = 1;
}

生成代码:

1
2
3
4
5
6
7
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

1
2
3
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 选项。

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
  rpc Test(TestRequest) returns (TestResponse) {
    option idempotency_level = IDEMPOTENCY_UNKNOWN;
    option (google.api.http) = {
      post: "/v1/test"
      body: "*"
    };
  }