Golang使用Protobuf
1、 概述
Protocol buffers 是语言中立、平台中立、可扩展的结构化数据序列化机制,就像 XML,但是它更小、更快、更简单。你只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码轻松地将结构化数据写入和读取各种数据流,支持各种语言。因为profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
Protocol buffers安装配置请参见:Mac下安装配置Protocol Buffers
2、如何使用protobuf呢?
-
定义了一种源文件,扩展名为
.proto
,使用这种源文件,可以定义存储类的内容(消息类型) -
protobuf有自己的编译器
protoc
,可以将.proto
编译成对应语言的文件,就可以进行使用了,对于Go,编译器为文件中每种消息类型生成一个.pb.go
文件。
3、protobuf "hello world" 示例
假设,我们现在需要传输用户信息,其中有username和age两个字段,创建文件user.proto,文件内容如下:
// 指定的当前proto语法的版本,有2和3 syntax = "proto3"; // option go_package = "path;name"; path 表示生成的go文件的存放地址,会自动生成目录 // name 表示生成的go文件所属的包名 option go_package="../service"; // 指定生成出来的文件的package package service; message User { string username = 1; int32 age = 2; }
运行protoc命令编译成go中间文件
# 编译user.proto之后输出到service文件夹 protoc --go_out=../service user.proto
项目结构:
测试:
package main import ( "Grpc-Protobuf/service" "fmt" "google.golang.org/protobuf/proto" ) func main() { user := &service.User{ Username: "zhangsan", Age: 20, } //转换为protobuf marshal, err := proto.Marshal(user) if err != nil { panic(err) } newUser := &service.User{} err = proto.Unmarshal(marshal, newUser) if err != nil { panic(err) } fmt.Println(newUser.String()) }
输出:
username:"zhangsan" age:20
4、proto文件语法详解
4.1 版本
Protocol Buffers文档的第一个非注释行,为版本声明,不填写的话默认为版本2。
syntax = "proto3"; 或者 syntax = "proto2";
官方建议新项目采用proto3,proto3简化了proto2的开发,提高了开发的效能,但是也带来了版本不兼容的问题。老项目因为兼容性的问题继续使用proto2,并且会长时间的支持proto2。
4.2 包(package)
可以向.proto
文件中添加可选的package
,以防止协议消息类型之间的名称冲突,同名的Message可以通过package进行区分。
在没有为特定语言定义option xxx_package的时候,它还可以用来生成特定语言的包名,比如go package、Java package。
package foo.bar; message Open { ... }
然后你可以在定义你的消息类型的字段时使用包说明符:
message Foo { ... foo.bar.Open open = 1; ... }
包说明符对生成代码的影响取决于您选择的语言,在Go中,包用作Go包名称, 除非在.proto
文件中显式提供option go_package,由.proto编译生成go文件时,option go_package = "path;name"配置中name设置的包名会覆盖packag设置的包名。
4.3 选项(option)
go_package(文件选项):用于生成的Go包。如果在.proto文件中没有给出显式的go_package选项,那么默认情况下将使用proto包(使用.proto文件中的“package”关键字指定)。如果不生成Go代码,则此选项无效。格式如下:
option go_package = "path;name"; path
path表示生成的go文件的存放地址,会自动生成目录,name 表示生成的go文件所属的包名。如果.proto文件设置了package建议忽略name的配置,如果name也需要配置的话建议设置值和package值一致。
proto参数区分之package和option go_package参见:https://blog.csdn.net/zhangh571354026/article/details/123852629
4.4 消息类型(message)
protobuf
中定义一个消息类型是通过关键字message
字段指定的,消息就是需要传输的数据格式的定义。message可以包含多种类型字段(field),每个字段声明以分号结尾。message经过protoc编译后会生成对应的class类,field则会生成对应的方法。
message关键字类似于C++中的class,Java中的class,go中的struct
例如:
message User { string username = 1; int32 age = 2; }
在消息中承载的数据分别对应于每一个字段。
其中每个字段都有一个名字和一种类型 。
4.4.1 字段规则
-
required
:消息体中必填字段,不设置会导致编解码异常,此关键字可以忽略,例如message user中的username和age字段都是忽略required的必填字段。 -
optional
: 消息体中可选字段。 -
repeated
: 消息体中可重复字段,重复的值的顺序会被保留,在go中重复字段会被定义为切片。
message User { string username = 1; int32 age = 2; optional string password = 3; // 生成的是指针 repeated string address = 4; // 生产的是切片 }
4.4.2 字段映射
.proto Type | Notes | C++ Type | Python Type | Go Type |
---|---|---|---|---|
double | double | float | float64 | |
float | float | float | float32 | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代 | int32 | int | int32 |
uint32 | 使用变长编码 | uint32 | int/long | uint32 |
uint64 | 使用变长编码 | uint64 | int/long | uint64 |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | int | int32 |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的 int64高效。 | int64 | int/long | int64 |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。 | uint32 | int | uint32 |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。 | uint64 | int/long | uint64 |
sfixed32 | 总是4个字节 | int32 | int | int32 |
sfixed32 | 总是4个字节 | int32 | int | int32 |
sfixed64 | 总是8个字节 | int64 | int/long | int64 |
bool | bool | bool | bool | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。 | string | str/unicode | string |
bytes | 可能包含任意顺序的字节数据。 | string | str | []byte |
4.4.3 字段默认值
protobuf3 删除了 protobuf2 中用来设置默认值的 default 关键字,取而代之的是protobuf3为各类型定义的默认值,也就是约定的默认值,如下表所示:
类型 | 默认值 |
---|---|
bool | false |
整型 | 0 |
string | 空字符串"" |
枚举enum | 第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0; |
message | 不是null,而是DEFAULT_INSTANCE |
4.4.5 标识符
在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是[0,2^29-1]范围内的一个整数。
message Person { string name = 1; // (位置1) int32 id = 2; optional string email = 3; repeated string phones = 4; // (位置4) }
以Person为例,name=1,id=2, email=3, phones=4 中的1-4就是标识号。
[1-15]内的标识号在编码时只占用一个字节,包含标识符和字段类型,[16-2047]之间的标识符占用2个字节。建议为频繁出现的字段使用[1-15]间的标识符。
4.4.6 定义多个消息类型
一个proto文件中可以定义多个消息类型
message UserRequest { string username = 1; int32 age = 2; optional string password = 3; repeated string address = 4; } message UserResponse { string username = 1; int32 age = 2; optional string password = 3; repeated string address = 4; }
4.4.7 嵌套消息
可以在其他消息类型中定义、使用消息类型,在下面的例子中,Person消息就定义在PersonInfo消息内,如 :
message PersonInfo { message Person { string name = 1; int32 height = 2; repeated int32 weight = 3; } repeated Person info = 1; }
如果你想在它的父消息类型的外部重用这个消息类型,你需要以PersonInfo.Person的形式使用它,如:
message PersonMessage { PersonInfo.Person info = 1; }
当然,你也可以将消息嵌套任意多层,如 :
message Grandpa { // Level 0 message Father { // Level 1 message son { // Level 2 string name = 1; int32 age = 2; } } message Uncle { // Level 1 message Son { // Level 2 string name = 1; int32 age = 2; } } }
4.4.7 注释
.proto
文件的注释和golang语法一样,//
用作单行注释,/*...*/
用作多行注释。
4.5 定义服务(Service)
如果要在RPC(Remote Procedure Call,远程过程调用)系统中使用消息类型,可以在.proto
文件中定义RPC服务接口,协议缓冲区编译器将根据所选语言生成服务接口代码和存根。因此,例如,如果您想用一个方法定义一个RPC服务,该方法接受您的SearchRequest并返回一个SearchResponse,您可以在.proto文件中这样定义它:
service SearchService { //rpc 服务的函数名 (传入参数)返回(返回参数) rpc Search (SearchRequest) returns (SearchResponse); }
上述代表表示,定义了一个RPC服务,该方法接收SearchRequest返回SearchResponse。
与协议缓冲区一起使用的最直接的RPC系统是gRPC:Google开发的一个与语言和平台无关的开源RPC系统。gRPC与协议缓冲区配合得特别好,允许您使用特殊的协议缓冲区编译器插件直接从.proto文件生成相关的RPC代码。
4.6 使用其他消息类型(import)
可以将其他消息类型用作字段类型。例如,假设您希望在每个SearchResponse
消息中包含Result
消息——为此,您可以在同一.proto
中定义Result
消息类型,然后在SearchResponse
中指定类型为Result
的字段:
message SearchResponse { repeated Result results = 1; } message Result { string url = 1; string title = 2; repeated string snippets = 3; }
在上面的示例中,Result
消息类型与SearchResponse
在同一个文件中定义——如果要用作字段类型的消息类型已经在另一个.proto
文件中定义了呢?
通过导入其他.proto
文件,可以使用这些文件中的定义。要导入另一个.proto
的定义,请在文件顶部添加一个import
语句:
import "myproject/other_protos.proto";
默认情况下,只能使用直接导入的.proto
文件中的定义。但是,有时可能需要将.proto
文件移动到新位置。不用直接移动.proto
文件并在一次更改中更新所有import
调用,现在可以在旧位置放置一个伪.proto
文件,使用import public
概念将所有导入转发到新位置。任何导入包含import public
语句的proto
的文件都可以传递依赖关系。例如:
// new.proto // All definitions are moved here
// old.proto // This is the proto that all clients are importing. import public "new.proto"; import "other.proto";
// client.proto import "old.proto"; // You use definitions from old.proto and new.proto, but not other.proto
重要:协议编译器使用-I/--proto_path
路径标志在协议编译器命令行上指定的一组目录中搜索导入的文件。如果没有给定标志,它将在调用编译器的目录中查找。通常,您应该将--proto_path
标志设置为项目的根目录,并对所有导入使用完全限定名。
4.7 生成类(.proto->.pb.go)
要生成Go代码,需要使用.proto文件中定义的消息类型和服务,还需要在.proto上运行协议缓冲区编译器protoc。编译器和protobuf的golang插件安装请参见:Mac下安装配置Protocol Buffers
协议编译器的调用方式如下:
protoc --proto_path=IMPORT_PATH --go_out=DST_DIR path/to/file.proto
-
IMPORT_PATH:指定解析import指令时要在其中查找.proto文件的目录。如果省略,则使用当前目录。通过多次传递-proto_path选项可以指定多个导入目录;它们将按顺序进行搜索。-I=_IMPORT_PATH_可以用作--proto_path的缩写形式。
-
--go_out表示在DST_DIR中生成Go代码。
-
必须提供一个或多个.proto文件作为输入。可以一次指定多个.proto文件。尽管这些文件是相对于当前目录命名的,但每个文件都必须驻留在IMPORT_PATH导入的其中一个路径中,以便编译器可以确定其规范名称。
示例:
编译simple.proto,生成simple.pb.go
syntax = "proto3"; message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; }
在当前的目录下执行protoc -I=. -I/usr/local/include -I=$(GOPATH)/pkg --go_out=. simple.proto
, 可以将这个proto编译成Go的代码,因为这里我们使用了go_out
输出格式。
-I
指定protoc的搜索import的proto的文件夹。在MacOS
操作系统中protobuf把一些扩展的proto放在了/usr/local/include
对应的文件夹中(路径根据本地电脑实际存放目录配置,例如/usr/local/protobuf/include),一些第三方的Go库放在了gopath对应的包下,所以这里都把它们加上了。对于这个简单的例子,实际是不需要的。
参考:https://www.cnblogs.com/itheo/p/14272421.html
参考:https://juejin.cn/post/6844903666302844936
参考:https://colobu.com/2019/10/03/protobuf-ultimate-tutorial-in-go/