主要内容:protobug基础语法和生成go代码指南。
2001年初,Protobuf首先在Google内部创建, 我们把它称之为 proto1
,一直以来在Google的内部使用,其中也不断的演化,根据使用者的需求也添加很多新的功能,一些内部库依赖它。几乎每个Google的开发者都会使用到它。
Google开始开源它的内部项目时,因为依赖的关系,所以他们决定首先把Protobuf开源出去。 proto1在演化的过程中有些混乱,所以Protobuf的开发者重写了Protobuf的实现,保留了proto1的大部分设计,以及proto1的很多的想法。但是开源的proto2不依赖任何的Google的库,代码也相当的清晰。2008年7月7日,Protobuf开始公布出来。
Protobuf公布出来也得到了大家的广泛的关注, 逐步地也得到了大家的认可,很多项目也采用Protobuf进行消息的通讯,还有基于Protobuf的微服务框架GRPC。在使用的过程中,大家也提出了很多的意见和建议,Protobuf也在演化,于2016年推出了Proto3。 Proto3简化了proto2的开发,提高了开发的效能,但是也带来了版本不兼容的问题。
目前Protobuf的稳定版本是3.9.2,于2019年9月23日发布。由于很多公司很早的就采用了Protobuf,所以很多项目还在使用proto2协议,目前是proto2和proto3同时在使用的状态。
Protocol Buffer名称来自于初期一个主要的类的名称ProtocolBuffer
。
Google当前并没有Protobuf的相关专利,所以不必担心侵权的问题。
一个message
类型定义描述了一个请求或响应的消息格式,可以包含多种类型字段。
例如定义一个搜索请求的消息格式SearchRequest
,每个请求包含查询字符串、页码、每页数目。每个字段声明以分号结尾。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
SearchRequest
消息定义了三个字段(名称/值对),字段就是每个要包含在该类型消息中的部分数据。每个字段都具有名称和类型 。每一个字段后面有一个顺次的index,便于消息通信时解析消息内容。消息的字段可以是一下规则之一:
单个 .proto 文件中可以定义多个消息类型。这在定义相关联的多个消息中很有用——例如要定义与搜索消息SearchRequest
相对应的回复消息 SearchResponse
,则可以在同一个 .proto 文件中增加它的定义:
essage SearchRequest {
string query = 1; //请求消息
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
可以使用reserved
关键字指定保留字段名和标签。注意,不能在一个reserved
声明中混合字段名和标签。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
.proto
文件编译结果当使用protocol buffer编译器运行.proto
文件时,编译器将生成所选语言的代码,用于使用在.proto
文件中定义的消息类型、服务接口约定等。不同语言生成的代码格式不同:
.proto
文件生成一个.h
文件和一个.cc
文件,每个消息类型对应一个类.java
文件,同样每个消息对应一个类,同时还有一个特殊的Builder
类用于创建消息接口.proto
文件中的消息类型生成一个含有静态描述符的模块,该模块与一个元类metaclass在运行时创建需要的Python数据访问类.pb.go
文件,每个消息类型对应一个结构体.proto | C++ | Java | Python | Go |
---|---|---|---|---|
double | double | double | float | float64 |
float | float | float | float | float32 |
int32 | int32 | int | int | int32 |
int64 | int64 | long | ing/long[3] | int64 |
uint32 | uint32 | int[1] | int/long[3] | uint32 |
uint64 | uint64 | long[1] | int/long[3] | uint64 |
sint32 | int32 | int | intj | int32 |
sint64 | int64 | long | int/long[3] | int64 |
fixed32 | uint32 | int[1] | int | uint32 |
fixed64 | uint64 | long[1] | int/long[3] | uint64 |
sfixed32 | int32 | int | int | int32 |
sfixed64 | int64 | long | int/long[3] | int64 |
bool | bool | boolean | boolean | bool |
string | string | String | str/unicode[4] | string |
bytes | string | ByteString | str | []byte |
前面的例子中,我们将 Result
消息定义在了与 SearchResponse
相同的文件中——但若我们需要作为字段类型使用的消息类型已经定义在其他的 .proto 文件中了呢?
可以通过导入操作来使用定义在其他 .proto 文件中的消息定义。在文件的顶部使用 import 语句完成导入其他 .proto 文件中的定义:
import "myproject/other_protos.proto";
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto
文件中定义一个RPC服务接口,protocol编译器会根据所选择的不同语言生成服务接口代码。例如,想要定义一个RPC服务并具有一个方法,该方法接收SearchRequest
并返回一个SearchResponse
,此时可以在.proto
文件中进行如下定义:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse) {}
}
生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求。
最直接使用 protocal buffer 的 RPC 系统是 gRPC :一款 Google 开源,语言和平台无关的 RPC 系统。gRPC 对 protocol buffer 的支持非常好同时允许使用特定的 protocol buffer 编译器插件来基于 .proto 文件生成相关的代码。
1.描述文件以.proto
做为文件后缀,除结构定义外的语句以分号结尾
2.Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式:
message SongServerRequest {
required string song_name = 1;
}
3.Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式:
enum Foo {
FIRST_VALUE = 1;
SECOND_VALUE = 2;
}
4.Service名称与RPC方法名统一采用驼峰式命名。
主要描述protocol buffer编译器通过给定的.proto
会编译生成什么Go代码。
Protobuf核心的工具集是C++语言开发的,官方的protoc编译器中并不支持Go语言,需要安装一个插件才能生成Go代码。用如下命令安装:
go get github.com/golang/protobuf/protoc-gen-go
提供了一个protoc-gen-go
二进制文件,当编译器调用时传递了--go_out
命令行标志时protoc
就会使用它。--go_out
告诉编译器把Go源代码写到哪里。编译器会为每个.proto
文件生成一个单独的源代码文件。
输出文件的名称是通过获取.proto文件的名称并进行两处更改来计算的:
.pb.go
。比如说player_record.proto
编译后会得到player_record.pb.go
。--proto_path
或-I
命令行标志指定)将替换为输出路径(使用--go_out
标志指定)。当你运行如下编译命令时:
protoc --proto_path=src --go_out=build/gen src/foo.proto src/bar/baz.proto
编译器会读取文件src/foo.proto
和src/bar/baz.proto
,这将会生成两个输出文件build/gen/foo.pb.go
和build/gen/bar/baz.pb.go
如果有必要,编译器会自动生成build/gen/bar
目录,但是他不能创建build
或者build/gen
目录,这两个必须是已经存在的目录。
举个例子:
一个简单的消息声明:
message Foo {}
protocol buffer编译器将会生成一个名为Foo
的结构体,实现了proto.Message
接口的Foo
类型的指针。研究了一下工作中的代码,每一个proto文件生成的.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 Bar {}
复制代码
对于一个有Bar
类型字段的消息:
// proto3
message Baz {
Bar foo = 1;
}
复制代码
编译器将会生成一个Go结构体
type Baz struct {
Foo *Bar
}
复制代码
消息类型的字段可以设置为nil,这意味着该字段未设置,有效清除该字段。这不等同于将值设置为消息结构体的“空”实例。
编译器还生成一个func(m * Baz)GetFoo()* Bar
辅助函数。这让不在中间检查nil值进行链式调用成为可能。
每个重复的字段在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结构体。
参考:
1.https://juejin.im/post/5dbaf13ee51d452a0c707f86
2.https://juejin.im/post/5d81bb5cf265da03ae78ab7b
3.https://www.bookstack.cn/read/go-grpc/chapter1-protobuf.md
4.说明文档:https://www.bookstack.cn/read/go-grpc/chapter1-protobuf.md
5.全面protobuf与go:https://colobu.com/2019/10/03/protobuf-ultimate-tutorial-in-go/
6.https://halfrost.com/protobuf_encode/