Protobuf语法
Protocol Buffers(protobuf)是一种语言无关,平台无关,可扩展的用于序列化结构化数据的方式——类似XML,但比XML更灵活,更高效。
使用protobuf语法编写xxx.proto
文件,然后将其编译成可供特定语言识别和使用的代码文件,供程序调用,这是protobuf的基本工作原理。
xxx.proto
文件编译成xxx.pb.go
文件——一个普通的go代码文件。要使用protobuf,首先我们需要下载protobuf编译器——protoc,但Go语言并没有被编译器直接支持,而是通过插件的方式被编译器引用,所以同时我们还需要下载Go语言的编译插件:
go install google.golang.org/protobuf/cmd/protoc-gen-go
示例:
helloword.proto
syntax = "proto3";
option go_package = "proto-demo/quick_start/pb"; // 当前文件所在包 package pb; message SearchRequest { string query = 1; int32 page = 2; int32 page_size = 3; } message SearchResponse { int32 code = 1; string msg = 2; } service Search { rpc user(SearchRequest) returns (SearchResponse); }
常用命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld/helloworld.proto
// 这里可以将--go_out=. 和 --go_opt=paths=source_relative 进行合并使用
protoc --go_opt=paths=source_relative:. --go-grpc_out=. --go-grpc_opt=paths=source_relative helloword/helloword.proto
--proto_path 或 -I: 指定锁编译源码的搜索路径
生成基于go语言的*.pb.go文件
--go_out: go代码生成目录
--go_opt: import/source_relative
默认为 import,代表按照生成的 go代码的包的全路径去创建目录层级,
source_relative代表按照 proto 源文件的目录层级去创建 go 代码的目录层级,如果目录已存在则不用创建。
生成grpc文件
--go-grpc_out:
--go_grpc_opt:
protoc使用:
示例文件:
syntax = "proto3"; package greeter; option go_package="proto1/greeter"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
执行命令:
protoc --proto_path=. --go_out=. proto1/greeter/greeter.proto
$ protoc --help Usage: protoc [OPTION] PROTO_FILES -IPATH, --proto_path=PATH 指定搜索路径 --plugin=EXECUTABLE: .... --cpp_out=OUT_DIR Generate C++ header and source. --csharp_out=OUT_DIR Generate C# source file. --java_out=OUT_DIR Generate Java source file. --js_out=OUT_DIR Generate JavaScript source. --objc_out=OUT_DIR Generate Objective C header and source. --php_out=OUT_DIR Generate PHP source file. --python_out=OUT_DIR Generate Python source file. --ruby_out=OUT_DIR Generate Ruby source file @<filename> proto文件的具体位置
1.搜索路径参数
第一个比较重要的参数就是搜索路径参数
,即上述展示的-IPATH, --proto_path=PATH
。它表示的是我们要在哪个路径下搜索.proto
文件,这个参数既可以用-I
指定,也可以使用--proto_path=
指定。
如果不指定该参数,则默认在当前路径下进行搜索;另外,该参数也可以指定多次,这也意味着我们可以指定多个路径进行搜索。
比如下面这三条指令,表达的含义都是一样的
2.语言插件参数
语言参数即上述的--cpp_out=
,--python_out=
等,protoc支持的语言长达13种,且都是比较常见的。像上面出现的语言参数,说明protoc本身已经内置该语言对应的编译插件,我们无需安装。
而如果上面没出现的,比如--go_out=
,就得自己单独安装语言插件,比如--go_out=
对应的是protoc-gen-go
3.proto文件位置参数
proto文件位置参数即上述的@<filename>
参数,指定了我们proto文件的具体位置,如proto1/greeter/greeter.proto
。
proto文件(非protoc)有两个易混参数,即package
和xx_package
,xx指的是你的编译语言,比如你要编程成Go语言,对应的就是go_package
。
package
package
参数针对的是protobuf,是proto文件的命名空间,它的作用是为了避免我们定义的接口,或者message出现冲突。
举一个小栗子,假设我有A.proto
和B.proto
两份文件,如下
# A.proto
message UserInfo {
uint32 uid = 1;
string name = 2;
}
# B.proto
message UserInfo {
uint32 uid = 1;
string name = 2;
uint32 age = 3;
string work = 4;
}
如上,两份文件同时有一个UserInfo
的message,这时候如果我需要在A文件引用B文件,如果没有指定package,就无法区分是要调A的UserInfo
还是调B的。
xx_package
这里以go_package
进行举例说明,该参数主要声明Go代码的存放位置,也可以说它解决的是包名问题(因为proto文件编译后会生成一份.pb.go
文件,既然是go文件,就有包名问题)
.pb.go
常规的存放路径一般是放在同名proto文件下,但也有些人不想这么做,比如他想把所有.pb.go
文件都存放在一个特定文件夹下,比如上述的 pb_go
,那么他有两种办法:
第一种:
# 修改 --go_out,go_package 保持不变
$ protoc --proto_path=. --go_out=./proto1/pb_go proto1/greeter/greeter.proto
这样生成的pb文件在 pb_go/proto1/greeter 目录下,文件目录有点冗余,不过pb文件的包名仍然是 greeter
第二种:
# 修改 go_package, go_out 保持不变
option go_package="proto1/pb_go";
$ protoc --proto_path=. --go_out=. proto1/greeter/greeter_v2.proto
这样生成的pb文件在 pb_go 目录下,pb文件的包名为 pb_go
另外,xx_package
其实有两种声明方式,第一种是在文件中声明,你已经见过了;第二种是在命令行中声明,声明格式如下:M${PROTO_FILE}=${GO_IMPORT_PATH}
。
目前,官方文档比较推荐在文件中声明,目的是可以缩短protoc指令的长度,我个人也比较推荐这种方式。
好了,相信通过以上几个例子,你已经能大致弄懂package
和xx_package
的区别了,再次强调下,官方文档中已经说明,package
和xx_package
两者没有关联,属于不同范畴
--go_out详细解读
想必大家在使用的时候,应该遇到过这些写法:--go_out=paths=import:.
、--go_out=paths=source_relative:.
,或者--go_out=plugins=grpc:.
。
这样写表达的是啥意思呢?
所以我们需要知道,--go_out
参数是用来指定 protoc-gen-go 插件的工作方式和Go代码的生成位置,而上面的写法正是表明该插件的工作方式。
--go_out
主要的两个参数为plugins
和 paths
,分别表示生成Go代码所使用的插件,以及生成的Go代码的位置。--go_out
的写法是,参数之间用逗号隔开,最后加上冒号来指定代码的生成位置,比如--go_out=plugins=grpc,paths=import:.
。
paths
参数有两个选项,分别是 import
和 source_relative
,默认为 import,表示按照生成的Go代码的包的全路径去创建目录层级,source_relative 表示按照 proto源文件的目录层级去创建Go代码的目录层级,如果目录已存在则不用创建。
protobuf文件语法解析
syntax = "proto3"; option go_package = "examplepb"; // 编译后的golang包名 package example.everything; // proto包名 ...
go_package
和package
两个关于包的声明,但这两个package
表达的意义并不相同,package example.everything;
表明的是当前.proto
文件所在的包名,跟Go语言类似,在相同的包名下,不能定义相同名称的message
,enum
或是service
。option go_package = "examplepb"
则定义了一个文件级别的option
,用于指定编译后的golang包名。... import "google/protobuf/any.proto"; import "google/protobuf/descriptor.proto"; //import "other.proto"; ...
import
用于引入其他的proto文件,当在当前文件中要使用其他proto文件的定义时,需要将其import
进来,然后可以通过类似packageName.MessageName
的方式来引用需要的内容,跟Go语言的import
十分类似。执行编译protoc
的时候,需要加上-I
参数来指定import
文件的路径,例如: protoc -I $GOPATH/src --go_out=. a_bit_of_everything.proto
message:
// 普通的message message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; }
message
可以包含多个字段声明,每个字段声明需要包含字段类型,字段名称和一个唯一序号。
字段类型可以是标量,枚举或是其他message
类型。
唯一序号用于标识该字段在消息二进制编码中位置。
枚举类型
... // 枚举 enum enum Status { STATUS_UNSPECIFIED = 0; STATUS_OK = 1; STATUS_FAIL= 2; STATUS_UNKNOWN = -1; // 不推荐有负数 } ...
通过enum
关键字定义枚举类型,在protobuf中,枚举是一个int32类型。第一个枚举值必须从0开始,如果不希望在代码中使用0值,可以将第一个值用XXX_UNSPECIFIED
作为占位符。由于enum类型实际上是用protobuf的int32类型的编码方式编码,故不推荐在枚举类型中使用负数。
XXX_UNSPECIFIED
只是一种代码规范。并不影响代码行为。
保留字段 (Reserved Fields) & 保留枚举值(Reserved Values)
// 保留字段 message ReservedMessage { reserved 2, 15, 9 to 11; reserved "foo", "bar"; // string abc = 2; // 编译报错 // string foo = 3; // 编译报错 } // 保留枚举 enum ReservedEnum { reserved 2, 15, 9 to 11, 40 to max; reserved "FOO", "BAR"; // FOO = 0; // 编译报错 F = 0; }
如果我们将某message
中的字段删除了,后面更新可能会重新使用这些字段。当新旧两种proto定义都在线上运行时,编解码可能会发生错误。例如有新旧两个版本的Foo
:
// old version
message Foo {
string a = 1;
}
// new version
message Foo {
int32 a = 1;
}
如果使用新版本的proto来解析旧版本的消息,就会发生错误,因为新版本proto会尝试将a
解析成int32,但实际上旧版本proto是按照string类型来对a
进行编码的。protobuf通过提供reserved
关键字来避免新旧版本冲突的问题:
// new version
message Foo {
reserved 1; // 标记第一个字段是保留的
int32 a = 2; // 序号从2开始,就不会与旧版本的string类型a冲突了
}
// nested 嵌套message message SearchResponse { message Result { string url = 1 ; string title = 2; } enum Status { UNSPECIFIED = 0; OK = 1; FAIL= 2; } Result results = 1; Status status = 2; }
message
允许多层嵌套,message
和enum
都可以嵌套。被嵌套的message
和enum
不仅可以在当前message
中使用,也可以被其他message
引用:
message OtherResponse { SearchResponse.Result result = 1; SearchResponse.Status status = 2; }
复合类型
除标量类型外,protobuf还提供了一些非标量类型,在本文中我把它们统称为复合类型。
复合类型并不是官方划分的类别。是本文为了便于理解而归纳总结的一个概念。
repeated
// repeated message RepeatedMessage { repeated SearchRequest requests = 1; repeated Status status = 2; repeated int32 number = 3; }
repeated
可以作用在message
中的变量类型上。只有标量类型,枚举类型和message类型可以被repeated
修饰。repeated
表示当前修饰变量可以被重复任意次(包括0次),其实就是表示当前修饰类型的一个变长数组,也就是Go语言中的slice
:// repeated type RepeatedMessage struct { Requests []*SearchRequest `protobuf:"bytes,1,rep,name=requests,proto3" json:"requests,omitempty"` Status []Status `protobuf:"varint,2,rep,packed,name=status,proto3,enum=example.everything.Status" json:"status,omitempty"` Number []int32 `protobuf:"varint,3,rep,packed,name=number,proto3" json:"number,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` }
map:
message MapMessage{ map<string, string> message = 1; map<string, SearchRequest> request = 2; }
除了slice
,当然还有map
。其中key的类型可以是除去double
,float
,bytes
以外的标量类型,value的类型可以是任意标量类型,枚举类型和message类型。protobuf的map
编译成Go语言后也是用map
来表示:
... // map type MapMessage struct { Message map[string]string `protobuf:"bytes,1,rep,name=message,proto3" json:"message,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Request map[string]*SearchRequest `protobuf:"bytes,2,rep,name=request,proto3" json:"request,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } ...
any:
... import "google/protobuf/any.proto"; ... message AnyMessage { string message = 1; google.protobuf.Any details = 2; } ...
any
类型可以包含一个不需要指定类型的任意的序列化消息。要使用any
类型,需要import google/protobuf/any.proto
。any
类型字段的encode/decode交由各语言的运行时各自实现,例如在Go语言中可以这样读写any
类型的字段:... import "github.com/golang/protobuf/ptypes" ... func getSetAny() { fmt.Println("getSetAny") req := &examplepb.SearchRequest{ Query: "query", } // 将SearchRequest打包成Any类型 a, err := ptypes.MarshalAny(req) if err != nil { log.Println(err) return } // 赋值 anyMsg := &examplepb.AnyMessage{ Message: "any message", Details: a, } req = &examplepb.SearchRequest{} // 从Any类型中还原proto消息 err = ptypes.UnmarshalAny(anyMsg.Details, req) if err != nil { log.Println(err) } fmt.Println(" any:", req) }
one of
// one of message OneOfMessage { oneof test_oneof { string m1 = 1; int32 m2 =2; } }
oneof
来保证这样的行为。对oneof
中任意一个字段设值,都会将其他字段清空。例如对上述的例子,test_oneof
字段要么是string类型的m1,要么是int32类型的m2。在Go语言中读写oneof
的示例如下:func getSetOneof() { fmt.Println("getSetOneof") oneof := &examplepb.OneOfMessage{ // 同一时间只能设值一个值 TestOneof: &examplepb.OneOfMessage_M1{ M1: "this is m1", }, } fmt.Println(" m1:", oneof.GetM1()) // this is m1 fmt.Println(" m2:", oneof.GetM2()) // 0 }
options & extensions
相信大部的gopher在平常使用protobuf的过程中都很少关注options
,80%的开发工作也不需要直接用到options
。但options是一个很有用的功能,其大大提高了protobuf的扩展性,我们有必要了解它。options
其实是protobuf内置的一些message
类型,其分为以下几个级别:
- 文件级别(file-level options)
- 消息级别(message-level options)
- 字段级别(field-level options)
- service级别(service options)
- method级别(method options)
protobuf提供一些内置的options
可供选择,也提供了通过extend
关键字来扩展这些options
,达到增加自定义options
的目的。
在
proto2
语法中,extend
可以作用于任何message
,但在proto3
语法中,extend
仅能作用于这些定义option
的message
——仅用于自定义option
。
options
不会改变声明的整体含义(例如声明的是int32就是int32,不会因为一个option改变了其声明类型),但可能会影响在特定情况下处理它的方式。例如我们可以使用内置的deprecated option
将某字段标记为deprecated
:
message Msg { string foo = 1; string bar = 2 [deprecated = true]; //标记为deprecated。 }
当我们需要编写自定义protoc插件时,可以通过自定义options
为编译插件提供额外信息。
举个例子,假设我要开发一个proto的校验插件,其生成xxx.Validate()
方法来校验消息的合法性,我可以通过自定义options
来提供生成代码的必要信息:
message Msg { // required是自定义options,表示foo字段必须非空 string foo = 1; [required = true]; }
内置options
的定义可以在github.com/protocolbuf…找到,每种级别的options
都对应一个message
,分别是:
- FileOptions —— 文件级别
- MessageOptions —— 消息级别
- FieldOptions —— 字段级别
- ServiceOptions —— service级别
- MethodOptions —— method级别
以下将通过示例来逐一介绍这些级别的options
,以及如何扩展这些options
。
文件级别
... option go_package = "examplepb"; // 编译后的golang包名 ... message extObj { string foo_string= 1; int64 bar_int=2; } // file options extend google.protobuf.FileOptions { string file_opt_string = 1001; extObj file_opt_obj = 1002; } option (example.everything.file_opt_string) = "file_options"; option (example.everything.file_opt_obj) = { foo_string: "foo" bar_int:1 };
go_package
毫无疑问是protobuf内置提供的,用于指定编译后的golang包名。除了使用内置的外,可以通过extend
字段来扩展内置的FileOptions
,例如在上述例子中,我们新增了两个新的option——string类型的file_opt_string
和extObj类型的file_opt_obj
。并通过option
关键字设置了两个文件级别的options。在Go语言中,我们可以这样读取这些options:func getFileOptions() { fmt.Println("file options:") msg := &examplepb.MessageOption{} md, _ := descriptor.MessageDescriptorProto(msg) stringOpt, _ := proto.GetExtension(md.Options, examplepb.E_FileOptString) objOpt, _ := proto.GetExtension(md.Options, examplepb.E_FileOptObj) fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString) fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt) fmt.Println(" string:", *stringOpt.(*string)) } file options: obj.foo_string: foo obj.bar_int 1 string: file_options
消息级别
// message options extend google.protobuf.MessageOptions { string msg_opt_string = 1001; extObj msg_opt_obj = 1002; } message MessageOption { option (example.everything.msg_opt_string) = "Hello world!"; option (example.everything.msg_opt_obj) = { foo_string: "foo" bar_int:1 }; string foo = 1; }
与文件级别大同小异,不再赘述。Go语言读取示例:
func getMessageOptions() { fmt.Println("message options:") msg := &examplepb.MessageOption{} _, md := descriptor.MessageDescriptorProto(msg) objOpt, _ := proto.GetExtension(md.Options, examplepb.E_MsgOptObj) stringOpt, _ := proto.GetExtension(md.Options, examplepb.E_MsgOptString) fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString) fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt) fmt.Println(" string:", *stringOpt.(*string)) }
字段级别
// field options extend google.protobuf.FieldOptions { string field_opt_string = 1001; extObj field_opt_obj = 1002; } message FieldOption { // 自定义的option string foo= 1 [(example.everything.field_opt_string) = "abc",(example.everything.field_opt_obj) = { foo_string: "foo" bar_int:1 }]; // protobuf内置的option string bar = 2 [deprecated = true]; }
字段级别的option定义方式不使用option
关键字,格式为:用[]包裹的用逗号分隔的k=v形式的数组。在Go语言中,我们可以这样读取这些option:
func getFieldOptions() { fmt.Println("field options:") msg := &examplepb.FieldOption{} _, md := descriptor.MessageDescriptorProto(msg) stringOpt, _ := proto.GetExtension(md.Field[0].Options, examplepb.E_FieldOptString) objOpt, _ := proto.GetExtension(md.Field[0].Options, examplepb.E_FieldOptObj) fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString) fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt) fmt.Println(" string:", *stringOpt.(*string)) }
service和method级别
// service & method options extend google.protobuf.ServiceOptions { string srv_opt_string = 1001; extObj srv_opt_obj = 1002; } extend google.protobuf.MethodOptions { string method_opt_string = 1001; extObj method_opt_obj = 1002; } service ServiceOption { option (example.everything.srv_opt_string) = "foo"; rpc Search (SearchRequest) returns (SearchResponse) { option (example.everything.method_opt_string) = "foo"; option (example.everything.method_opt_obj) = { foo_string: "foo" bar_int: 1 }; }; }
option
关键字来定义,与文件级别和消息级别option类似,不再赘述。Go语言读取示例:func getServiceOptions() { fmt.Println("service options:") msg := &examplepb.MessageOption{} md, _ := descriptor.MessageDescriptorProto(msg) srv := md.Service[1] // ServiceOption stringOpt, _ := proto.GetExtension(srv.Options, examplepb.E_SrvOptString) fmt.Println(" string:", *stringOpt.(*string)) } func getMethodOptions() { fmt.Println("method options:") msg := &examplepb.MessageOption{} md, _ := descriptor.MessageDescriptorProto(msg) srv := md.Service[1] // ServiceOption objOpt, _ := proto.GetExtension(srv.Method[0].Options, examplepb.E_MethodOptObj) stringOpt, _ := proto.GetExtension(srv.Method[0].Options, examplepb.E_MethodOptString) fmt.Println(" obj.foo_string:", objOpt.(*examplepb.ExtObj).FooString) fmt.Println(" obj.bar_int", objOpt.(*examplepb.ExtObj).BarInt) fmt.Println(" string:", *stringOpt.(*string)) }
protoc-go-inject-tag
一般使用protoc生成的*.pb.go文件中结构体中protobuf中的tag数据是固定的, 使用该插件完成结构体的tag的自定义
github: https://github.com/favadi/protoc-go-inject-tag
1.安装并加入环境变量
go install github.com/favadi/protoc-go-inject-tag@latest
2.在proto文件中加入注释信息
test.proto
// file: test.proto syntax = "proto3"; package pb; option go_package = "/pb"; message IP { // @gotags: valid:"ip" string Address = 1; // Or: string MAC = 2; // @gotags: validate:"omitempty" }
使用protoc生成test.pb.go文件
protoc --proto_path=. --go_out=paths=source_relative:. test.proto
3.替换test.pb.go文件中的内容
$ protoc-go-inject-tag -input=./test.pb.go # or $ protoc-go-inject-tag -input="*.pb.go"
注: 在v1.3.0版本之后, 已经不推荐使用@inject_tags了, 使用最新提供的@gotags