文章目录
- 一、介绍
- 二、安装
- 三、protoc3语法
- 1、 protoc3 与 protoc2区别
- 2、proto3生成go代码
- 包
- Message
- 内嵌Message
- 字段
- 单一标量字段
- 单一message字段
- 可重复字段slice
- map字段
- 枚举
一、介绍
Protobuf
是Google
旗下的一款平台无关,语言无关,可扩展的序列化结构数据格式。所以很适合用做数据存储和作为不同应用,不同语言之间相互通信的数据交换格式,只要实现相同的协议格式,即同一proto
文件被编译成不同的语言版本,加入到各自的工程中去,这样不同语言就可以解析其他语言通过Protobuf
序列化的数据。目前官网提供了C++,Python,JAVA,GO
等语言的支持。
二、安装
- Mac上安装Protoc3
Windows
安装Protoc3
:可以查看本人的另一篇博客:windows安装protoc、protoc-gen-go、protoc-gen-go-grpc- 安装
protoc-gen-go
protoc-gen-go
是生成Go
代码的protocolbuffers
编译器。可以理解为一个编译器插件,配合protoc
来使用。在命令行执行如下命令即可完成安装:
go get -u github.com/golang/protobuf/protoc-gen-go@latest
网上资料推荐的基本都是这个命令,但目前该模块已被弃用,继续使用该命令将出现错误,提示该库已经被弃用,让我们使用go get -u google.golang.org/protobuf/
protoc-gen-go
二进制文件,当编译器调用时传递了 --go_out
命令行标志时, protoc
就会使用它。--go_out
告诉编译器把生成的Go
源代码写到哪里。编译器会为每个 .proto
文件生成一个单独的源代码文件。
输出文件的名称是通过获取.proto
文件的名称并进行两处更改来计算的:
生成文件的扩展名是 .pb.go
。比如说 player_record.proto
编译后会得到 player_record.pb.go
。
使用 --proto_path
或 -I
命令行标志指定proto
文件所在路径,使用 --go_out
标志指定生成的go
文件放在哪个路径下。
当你运行如下编译命令时:
protoc --proto_path=src --go_out=build/gen src/foo.proto src/bar/baz.proto
编译器会读取文件 src
目录下的src/foo.proto
和 src/bar/baz.proto
,这将会在build/gen
目录下生成两个输出文件 build/gen/foo.pb.go
和 build/gen/bar/baz.pb.go
如果有必要,编译器会自动生成 build/gen/bar
目录,其中bar
为我们在proto
文件中指定的go
文件包名,如option go_package = "/bar";
,但是他不能创建 build
或者 build/gen
目录,这两个必须是已经存在的目录。
三、protoc3语法
1、 protoc3 与 protoc2区别
proto3
在proto2
的基础上去掉了一些复杂的语法和特性,更强调约定而弱化语法。主要几点区别如下:
proto
文件第一行非空白非注释行,必须指定版本,syntax = "proto3"
;如果不指定,则默认是proto2
- 字段规则移除了
required
,并把optional
改名为singular
,省略不写时,默认就是singular
。 repeated
字段默认采用packed
编码,在proto2
中,需要明确使用[packed=true]
来为字段指定比较紧凑的packed
编码方式。- 语言增加
Go、Ruby、JavaNano
支持,即在proto2
时并不支持go
语言。 - 移除了
default
选项,在proto2
中,可以使用default
选项为某一字段指定默认值。在proto3
中,字段的默认值只能根据字段类型由系统决定。也就是说,默认值全部是约定好的,而不再提供指定默认值的语法。在字段被设置为默认值的时候,该字段不会被序列化。这样可以节省空间,提高效率。
但这样就无法区分某字段是根本没赋值,还是赋值了默认值。 所以一般需要避免将默认值作为任何行为的触发方式。例如
enum AudienceDisplayTypeEnum {
NoValue = 0; // (占位符)说明端上没有传入此参数,请勿使用
CurrentCount = 1; // 展示当前直播间内人数
AccumulativeCount = 2; // 展示直播间累计人数
SettingEntranceClosed = 99; // 端上拿到则不展示此选项,相当于配置项是否出现的开关
}
此例子是控制直播间展示在线人数还是看播人次的开关,开关仅两个取值:true
和false
,但这里并没有使用bool
类型,因为bool
型默认值是false
,即使前端没有给我们传该值,我们也会拿到false
值,从而可能当成是前端传过来的值,切换开关,因此使用枚举。使用枚举后,不使用0
和1
表示开关的打开与关闭,因为0
是枚举的默认值,也不应该作为控制行为的值,因此有业务含义的从序号为1
的字段开始。
6. 枚举类型的第一个字段必须为 0
,因为枚举会把第一个字段作为默认值
7. 增加了JSON
映射特性,如
message XXXRequest {
string name = 1;
int64 begin_time = 2 (go.tag = "json:\"beginTime\"");
int64 end_time = 3;
int32 page_no = 4;
int32 page_size = 5;
}
2、proto3生成go代码
Go Proto Buffer代码生成官网文档地址
包
如果一个.proto
文件中有包声明,生成的源代码将会使用它来作为Go
的包名,如果.proto
的包名中有.
,在Go
包名中会将.
转换为_
。举例来说proto
包名example.high_score
将会生成Go
包名example_high_score
。
在.proto
文件中可以使用option go_package
指令来覆盖上面默认生成Go
包名的规则。比如说包含如下指令的一个.proto
文件
package example.high_score;
option go_package = "/test";
生成的Go
源代码的包名是test
。
如果一个.proto
文件中不包含package
声明,生成的源代码将会使用.proto
文件的文件名作为Go
包名,.
会被首先转换为_
。举例来说一个名为high.score.proto
不包含package
声明的文件将会生成文件high.score.pb.go
,他的Go包名是high_score
。
Message
一个简单的message
声明:
message Foo {}
protocol buffer
编译器将会生成一个名为Foo
的结构体,var A *Foo
为实现了proto.Message
接口的Foo
类型的指针,因为Foo
实现了proto.Message
接口中ProtoMessage()
方法,生成的XXX.pb.go
文件将包含如下代码片段,注意看注释哦
type Foo struct {
}
// 重置proto为默认值
func (m *Foo) Reset() { *m = Foo{} }
// String 返回proto的字符串表示
func (m *Foo) String() string { return proto.CompactTextString(m) }
// ProtoMessage作为一个tag 确保其他人不会意外的实现
// proto.Message 接口.
func (*Foo) ProtoMessage() {}
内嵌Message
一个message
可以声明在其他message
的内部。比如:
message Foo {
message Bar {
}
}
这种情况,编译器会生成两个结构体:Foo
和Foo_Bar
。
字段
编译器会为每个在message
中定义的字段生成一个Go
结构体的字段,字段的确切性质取决于它的类型以及它是singular,repeated,map
还是oneof
字段。
注意生成的Go
结构体的字段将始终使用驼峰命名,即在.proto
文件中消息字段用的是小写加下划线(工作中基本都是这种形式),生成的Go
代码会是大驼峰命名。大小写转换的原理如下:
- 首字母会大写,如果
message
中字段的第一个字符是_
,它将被替换为X
。 - 如果内部下划线后跟小写字母,则删除下划线,并将后面跟随的字母大写。
因此,proto
字段foo_bar_baz
在Go
中变成FooBarBaz
,_my_field_name
变为XMyFieldName
。
单一标量字段
对于包级别字段定义:
int32 id = 1;
编译器将生成一个带有名为Id
的int32
字段和一个访问器方法GetId()
的结构,该方法返回结构体中Id
字段的零值(如果字段未设置(数值型零值为0
,字符串为空字符串))。
单一message字段
给出如下消息类型
message Bar {}
对于一个有Bar
类型字段的消息:
// proto3
message Baz {
Bar foo = 1;
}
编译器将会生成一个Go
结构体
type Baz struct {
Foo *Bar
}
消息类型的字段可以设置为nil
,这意味着该字段未设置。
编译器还生成一个func(m *Baz)GetFoo() *Bar
辅助函数。这让不在中间检查nil
值进行链式调用成为可能,因为该方法中会进行相关字段的nil
判断。
可重复字段slice
每个重复的字段在Go
中的结构中生成一个T
类型的slice
,其中T
是字段的元素类型。对于带有重复字段的消息:
message Baz {
repeated Bar foo = 1;
}
编译器会生成如下结构体:
type Baz struct {
Foo []*Bar
}
同样,对于字段定义repeated bytes foo = 1;
编译器将会生成一个带有类型为[][]byte
, 名为Foo
的字段的Go
结构体。对于可重复的枚举repeated MyEnum bar = 2;
,编译器会生成带有类型为[]MyEnum
, 名为Bar
的字段的Go
结构体。
map字段
每个映射字段会在Go
的结构体中生成一个map[TKey]TValue
类型的字段,其中TKey
是字段的键类型,TValue
是字段的值类型。对于下面这个消息定义:
message Bar {}
message Baz {
map<string, Bar> foo = 1;
}
编译器生成Go
结构体
type Baz struct {
Foo map[string]*Bar
}
枚举
给出如下枚举
message SearchRequest {
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 1;
}
编译器将会生成一个枚举类型和一系列该类型的常量。
对于消息中的枚举(像上面那样),类型名字以消息名开头
type SearchRequest_Corpus int32
对于包级别的枚举:
// .proto
enum Foo {
DEFAULT_BAR = 0;
BAR_BELLS = 1;
BAR_B_CUE = 2;
}
Go
中的类型不会对proto
中的枚举名称进行修改:
type Foo int32
此类型具有String()
方法,该方法返回给定值的名称。
Enum()
方法使用给定值初始化新分配的内存并返回相应的指针:
func (Foo) Enum() *Foo
编译器为枚举中的每个值生成一个常量。对于消息中的枚举,常量以消息的名称开头:
const (
SearchRequest_UNIVERSAL SearchRequest_Corpus = 0
SearchRequest_WEB SearchRequest_Corpus = 1
SearchRequest_IMAGES SearchRequest_Corpus = 2
SearchRequest_LOCAL SearchRequest_Corpus = 3
SearchRequest_NEWS SearchRequest_Corpus = 4
SearchRequest_PRODUCTS SearchRequest_Corpus = 5
SearchRequest_VIDEO SearchRequest_Corpus = 6
)
对于包级别的枚举,常量以枚举名称开头:
const (
Foo_DEFAULT_BAR Foo = 0
Foo_BAR_BELLS Foo = 1
Foo_BAR_B_CUE Foo = 2
)
protobuf
编译器还生成从整数值到字符串名称的映射以及从名称到值的映射:
var Foo_name = map[int32]string{
0: "DEFAULT_BAR",
1: "BAR_BELLS",
2: "BAR_B_CUE",
}
var Foo_value = map[string]int32{
"DEFAULT_BAR": 0,
"BAR_BELLS": 1,
"BAR_B_CUE": 2,
}