protocol buffers使用指南
承蒙大家厚爱,我的《Go语言之路》的纸质版图书已经上架京东,有需要的朋友请点击 此链接 购买。
protobuf是一种高效的数据格式,平台无关、语言无关、可扩展,可用于 RPC 系统和持续数据存储系统。
protocol buffers
protobuf介绍
Protobuf全称Protocol Buffer
,是 Google 公司于2008年开源的一种语言无关、平台无关、可扩展的用于序列化结构化数据——类似于XML,但比XML更小、更快、更简单,它可用于(数据)通信协议、数据存储等。你只需要定义一次你想要的数据结构,然后你就可以使用特殊生成的源代码来轻松地从各种数据流和各种语言中写入和读取你的结构化数据。目前 Protobuf 被广泛用作微服务中的通信协议。
protobuf语法
protobuf v3语法官方文档 protobuf v3中文语法指南
protobuf 编译器指南
安装 protobuf
从官方仓库:https://github.com/google/protobuf/releases 下载适合你平台的预编译好的二进制文件(protoc-<version>-<platform>.zip
)。
- 适用Windows 64位protoc-3.20.1-win64.zip
- 适用于Mac Intel 64位protoc-3.20.1-osx-x86_64.zip
- 适用于Mac ARM 64位protoc-3.20.1-osx-aarch_64.zip
- 适用于Linux 64位protoc-3.20.1-linux-x86_64.zip
例如,我使用 Intel 芯片的 Mac 系统则下载 protoc-3.20.1-osx-x86_64.zip
文件,解压之后得到如下内容。
其中:
- bin 目录下的 protoc 是可执行文件。
- include 目录下的是 google 定义的
.proto
文件,我们import "google/protobuf/timestamp.proto"
就是从此处导入。
我们需要将下载得到的可执行文件protoc
所在的 bin 目录加到我们电脑的环境变量中。
生成Go代码
编译器调用
protocol buffer编译器需要一个插件来根据提供的proto文件生成 Go 代码,Go1.16+请使用下面的命令安装插件。
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
这个命令将在 $GOBIN
中安装一个 protocol-gen-go
的二进制文件。我们需要确保 $GOBIN
在你的环境变量中,protocol buffer编译器才能找到它(可以通过go env
命令查看$GOPATH
)。
当使用go_out
标志调用 protoc 时,protocol buffer编译器将生成 Go 代码。protocol buffer编译器会将生成的Go代码输出到命令行参数go_out
指定的位置。go_out
标志的参数是你希望编译器编写 Go 输出的目录。编译器为每个.proto
文件输入创建一个源文件。输出文件的名称是通过将.proto
扩展名替换为.pb.go
而创建的。
生成的.pb.go
文件放置的目录取决于编译器标志。有以下几种输出模式:
paths=import
:输出文件放在以 Go 包的导入路径命名的目录中。例如,protos/buzz.proto
文件中带有example.com/project/protos/fizz
的导入路径,则输出的生成文件会保存在example.com/project/protos/fizz/buzz.pb.go
。如果未指定路径标志,这就是默认输出模式。module=$PREFIX
:输出文件放在以 Go 包的导入路径命名的目录中,但是从输出文件名中删除了指定的目录前缀。例如,输入文件pros/buzz.proto
,其导入路径为example.com/project/protos/fizz
并指定example.com/project
为module
前缀,结果会产生一个名为pros/fizz/buzz.pb.go
的输出文件。在module路径之外生成任何 Go 包都会导致错误。此模式对于将生成的文件直接输出到 Go 模块非常有用。paths=source_relative
:输出文件与输入文件放在相同的相对目录中。例如,一个protos/buzz.proto
输入文件会产生一个位于protos/buzz.pb.go
的输出文件。
在调用protoc
时,通过传递 go_opt
标志来提供特定于 protocol-gen-go
的标志位参数。可以传递多个go_opt
标志位参数。例如,当执行下面的命令时:
protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
编译器将从 src
目录中读取输入文件 foo.proto
和 bar/baz.proto
,并将输出文件 foo.pb.go
和 bar/baz.pb.go
写入 out
目录。如果需要,编译器会自动创建嵌套的输出子目录,但不会创建输出目录本身。
package
为了生成 Go 代码,必须为每个 .proto
文件(包括那些被生成的 .proto
文件传递依赖的文件)提供 Go 包的导入路径。有两种方法可以指定 Go 导入路径:
- 通过在
.proto
文件中声明它。 - 通过在调用
protoc
时在命令行上声明它。
我们建议在 .proto
文件中声明它,以便 .proto
文件的 Go 包可以与 .proto
文件本身集中标识,并简化调用 protoc
时传递的标志集。 如果给定 .proto
文件的 Go 导入路径由 .proto
文件本身和命令行提供,则后者优先于前者。
Go 导入路径是在 .proto
文件中指定的,通过声明带有 Go 包的完整导入路径的 go_package
选项来创建 proto 文件。用法示例:
option go_package = "example.com/project/protos/fizz";
调用编译器时,可以在命令行上指定 Go 导入路径,方法是传递一个或多个 M${PROTO_FILE}=${GO_IMPORT_PATH}
标志位。用法示例:
protoc --proto_path=src \
--go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz \
--go_opt=Mprotos/bar.proto=example.com/project/protos/foo \
protos/buzz.proto protos/bar.proto
由于所有 .proto
文件到其 Go 导入路径的映射可能非常大,这种指定 Go 导入路径的模式通常由控制整个依赖树的某些构建工具(例如 Bazel)执行。 如果给定的 .proto
文件有重复条目,则指定的最后一个条目优先。
对于 go_package
选项和 M
标志位,它们的值可以包含一个显式的包名称,该名称与导入路径之间用分号分隔。 例如:“example.com/protos/foo;package_name”
。 不鼓励这种用法,因为默认情况下包名称将以合理的方式从导入路径派生。
导入路径用于确定一个 .proto
文件导入另一个 .proto
文件时必须生成哪些导入语句。 例如,如果 a.proto
导入 b.proto
,则生成的 a.pb.go
文件需要导入包含生成的 b.pb.go
文件的 Go 包(除非两个文件在同一个包中)。 导入路径也用于构造输出文件名。 有关详细信息,请参阅上面的“编译器调用”部分。
Go 导入路径和 .proto
文件中的package
说明符之间没有关联。 后者仅与 protobuf 命名空间相关,而前者仅与 Go 命名空间相关。 此外,Go 导入路径和 .proto
导入路径之间没有关联。
Go语言使用protoc示例
我们新建一个名为demo
的项目,并且将项目中定义的.proto
文件都保存在proto
目录下。
本文后续的操作命令默认都在demo
目录下执行。
普通编译
下面的示例中我们将定义一个单独的proto
文件并进行编译。
定义proto
新建一个price.proto
文件。
// proto/book/price.proto
syntax = "proto3";
package book;
// 声明生成Go代码的导入路径(import path)
option go_package = "github.com/Q1mi/demo/proto/book";
message Price {
int64 market_price = 1; // 建议使用下划线的命名方式
int64 sale_price = 2;
}
我们在这个文件中使用option go_package = "github.com/Q1mi/demo/proto/book"
语句声明了生成的Go代码的导入路径。
项目当前的目录结构如下:
demo
└── proto
└── book
└── price.proto
生成代码
假设我们想把最终生成的Go代码还保存在proto
文件夹中,那么就可以执行下面的命令。
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/price.proto
其中:
--proto_path=proto
表示从proto目录下读取proto文件。--go_out=proto
表示生成的Go代码保存的路径。--go_opt=paths=source_relative
表示输出文件与输入文件放在相同的相对目录中。book/price.proto
表示在proto目录下的book/price.proto
文件。
此外,--proto_path
有一个别名-I
,上述编译命令也可以这样写。
protoc -I=proto --go_out=proto --go_opt=paths=source_relative book/price.proto
执行上述命令将会在proto
目录下生成book/price.pb.go
文件。
demo
└── proto
└── book
├── price.pb.go
└── price.proto
此处如果不指定--proto_path
参数那么编译命令可以简写为:
protoc --go_out=. --go_opt=paths=source_relative proto/book/price.proto
上面的命令都是将代码生成到demo/proto
目录,如果想要将生成的Go代码保存在其他文件夹中(例如pb
文件夹),那么我们需要先在demo
目录下创建一个pb
文件夹。然后在命令行通过--go_out=pb
指定生成的Go代码保存的路径。完整命令如下:
protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative book/price.proto
执行上面的命令便会在demo/pb
文件夹下生成Go代码。
demo
├── pb
│ └── book
│ └── price.pb.go
└── proto
└── book
├── price.pb.go
└── price.proto
import同目录下protobuf文件
随着业务的复杂度上升,我们可能会定义多个.proto
源文件,然后根据需要引入其他的protobuf文件。
在这个示例中,我们在demo/proto/book
目录下新建一个book.proto
文件,它通过import "book/price.proto";
语句引用了同目录下的price.proto
文件。
// demo/proto/book/book.proto
syntax = "proto3";
// 声明protobuf中的包名
package book;
// 声明生成的Go代码的导入路径
option go_package = "github.com/Q1mi/demo/proto/book";
// 引入同目录下的protobuf文件(注意起始位置为proto_path的下层)
import "book/price.proto";
message Book {
string title = 1;
Price price = 2;
}
编译命令如下:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto
这里有几点需要注意:
因为我们通过编译命令指定
--proto_path=proto
,所以import导入语句需要从demo/proto
文件夹的下层目录book
这一层开始写。因为导入的
price.proto
与book.proto
同属于一个package book;
,所以可以直接使用price
作为类型,无需添加 package 前缀(即无需写成book.price
)。
上述编译命令最终会生成demo/proto/book/book.pb.go
文件。
demo
└── proto
└── book
├── book.pb.go
├── book.proto
├── price.pb.go
└── price.proto
import其他目录下文件
我们在demo/proto
目录下新建了一个author
文件夹,用来存放与 author 相关的 protobuf 文件。例如我们定义一个表示作者信息的author.proto
文件,其内容如下:
// demo/proto/author/author.proto
syntax = "proto3";
// 声明protobuf中的包名
package author;
// 声明生成的Go代码的导入路径
option go_package = "github.com/Q1mi/demo/proto/author";
message Info {
string name = 1;
}
此时的目录结构:
demo
└── proto
├── author
│ └── author.proto
└── book
├── book.pb.go
├── book.proto
├── price.pb.go
└── price.proto
假设我们的 book 需要增加一个作者信息的字段——authorInfo
,这时我们需要在demo/proto/book/book.proto
中导入其他目录下的 author.proto
文件。具体改动如下。
// proto/proto/book/book.proto
syntax = "proto3";
// 声明protobuf中的包名
package book;
// 声明生成的Go代码的导入路径
option go_package = "github.com/Q1mi/demo/proto/book";
// 引入同目录下的protobuf文件(注意起始位置为proto_path的下层)
import "book/price.proto";
// 引入其他目录下的protobuf文件
import "author/author.proto";
message Book {
string title = 1;
Price price = 2;
author.Info authorInfo = 3; // 需要带package前缀
}
我们通过import "author/author.proto";
导入了author
包的author.proto
文件,所以在book
包下使用Info
类型时需要添加其包名前缀即author.Info
。
编译命令如下:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
此时的目录结构:
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
└── book
├── book.pb.go
├── book.proto
├── price.pb.go
└── price.proto
import google proto文件
有时候我们也需要在我们定义的 protobuf 文件中使用 Google 定义的类型,例如Timestamp
、Any
等。
例如我们要为我们的 book 添加出版日期——date
字段,就可以通过 import "google/protobuf/timestamp.proto";
导入并使用Timestamp
类型了。
修改后的book.proto
文件内容如下:
// demo/proto/book/book.proto
syntax = "proto3";
// 声明protobuf中的包名
package book;
// 声明生成的Go代码的导入路径
option go_package = "github.com/Q1mi/demo/proto/book";
// 引入同目录下的protobuf文件(注意起始位置为proto_path的下层)
import "book/price.proto";
// 引入其他目录下的protobuf文件
import "author/author.proto";
// 引入google/protobuf/timestamp.proto文件
import "google/protobuf/timestamp.proto";
message Book {
string title = 1;
Price price =2;
author.Info authorInfo = 3; // 需要带package前缀
// Timestamp是大写T!大写T!大写T!
google.protobuf.Timestamp date = 4; // 注意包名前缀
}
那么这个 google/protobuf/timestamp.proto
是从哪里导入的呢?
通常我们下载 protobuf编译器的时候会解压得到如下文件:
其中:
- bin 目录下的 protoc 是可执行文件。
- include 目录下的是 google 定义的
.proto
文件,我们import "google/protobuf/timestamp.proto"
就是从此处导入。
我们需要将下载得到的可执行文件protoc
所在的 bin 目录加到我们电脑的环境变量中。
如果你不是通过这种方式安装的 protobuf 那么你也可以手动将 Google 定义的protobuf文件下载到本地(git clone或者go get,protobuf文件在src下),然后通过 --proto_path
指定其路径。
protoc --proto_path=/Users/liwenzhou/workspace/go/pkg/mod/github.com/protocolbuffers/protobuf@v3.21.2+incompatible/src/ --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
或者你还可以简单粗暴的把下载好的 protobuf 文件拷贝到你项目的 proto 目录下。
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
├── book
│ ├── book.pb.go
│ ├── book.proto
│ ├── price.pb.go
│ └── price.proto
└── google
└── protobuf
└── timestamp.proto
然后执行下面的编译命令:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
生成gRPC代码
由于通常我们都是配合 gRPC 来使用 protobuf ,所以我们也需要基于.proto
文件生成Go代码的同时生成 gRPC 代码。
要想生成 gRPC 代码就需要先安装 protoc-gen-go-grpc
插件。
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
上述命令会默认将插件安装到$GOPATH/bin
,为了protoc
编译器能找到这些插件,请确保你的$GOPATH/bin
在环境变量中。
假设我们现在要提供一个创建书籍的 RPC 方法,那么我在book.proto
中添加如下定义。
// demo/proto/book/book.proto
// ...省略...
service BookService{
rpc Create(Book)returns(Book);
}
然后在 protoc 的编译命令添加 gRPC相关输出的参数,完整命令如下。
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
上述命令就会生成book_grpc.pb.go
文件。
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
└── book
├── book.pb.go
├── book.proto
├── book_grpc.pb.go
├── price.pb.go
└── price.proto
gRPC-Gateway
gRPC-Gateway 也是日常开发中比较常用的一个工具,它同样也是根据 protobuf 生成相应的代码。
安装工具
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
为protobuf文件添加注释
我们在book.proto
文件中添加如下注释。
// demo/proto/book/book.proto
syntax = "proto3";
// 声明protobuf中的包名
package book;
// 声明生成的Go代码的导入路径
option go_package = "github.com/Q1mi/demo/proto/book";
// 引入同目录下的protobuf文件(注意起始位置为proto_path的下层)
import "book/price.proto";
// 引入其他目录下的protobuf文件
import "author/author.proto";
// 引入google/protobuf/timestamp.proto文件
import "google/protobuf/timestamp.proto";
// 引入google/api/annotations.proto文件
import "google/api/annotations.proto";
message Book {
string title = 1;
Price price = 2;
author.Info authorInfo = 3; // 需要带package前缀
// Timestamp是大写T!大写T!大写T!
google.protobuf.Timestamp date = 4; // 注意包名前缀
}
service BookService{
rpc Create(Book)returns(Book){
option (google.api.http) = {
post: "/v1/book"
body: "*"
};
};
}
此时,我们又引入了google/api/annotations.proto
,这个文件是由googleapi定义在https://github.com/googleapis/googleapis。
想要在项目中引入上述protobuf源文件可以像上面引入timestamp.proto
文件一样将这个库下载到本地然后通过--proto_path
指定,或者直接把用到的 protobuf 源文件拷贝到我们的项目中。
本示例就采用第二种方法把此处用到的google/api/annotations.proto
文件和http.proto
文件拷贝到项目的google/api
目录下(annotations.proto
文件中引入了http.proto
文件)。
此时的项目目录如下:
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
├── book
│ ├── book.pb.go
│ ├── book.proto
│ ├── book_grpc.pb.go
│ ├── price.pb.go
│ └── price.proto
└── google
└── api
├── annotations.proto
└── http.proto
编译
这一次编译命令在之前的基础上要继续加上 gRPC-Gateway相关的 --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative
参数。
完整的编译命令如下:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
最终会编译得到一个book.pb.gw.go
文件。
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
├── book
│ ├── book.pb.go
│ ├── book.pb.gw.go
│ ├── book.proto
│ ├── book_grpc.pb.go
│ ├── price.pb.go
│ └── price.proto
└── google
└── api
├── annotations.proto
└── http.proto
为了方便编译可以在项目下定义Makefile
。
.PHONY: gen help
PROTO_DIR=proto
gen:
protoc \
--proto_path=$(PROTO_DIR) \
--go_out=$(PROTO_DIR) \
--go_opt=paths=source_relative \
--go-grpc_out=$(PROTO_DIR) \
--go-grpc_opt=paths=source_relative \
--grpc-gateway_out=$(PROTO_DIR) \
--grpc-gateway_opt=paths=source_relative \
$(shell find $(PROTO_DIR) -iname "*.proto")
help:
@echo "make gen - 生成pb及grpc代码"
后续想要编译只需在项目目录下执行make gen
即可。
管理 protobuf
在企业的项目开发中,我们通常会把 protobuf 文件存储到一个单独的代码库中,并在具体项目中通过git submodule
引入。这样做的好处是能够将 protobuf 文件统一管理和维护,避免因 protobuf 文件改动导致的问题。
本文示例代码已上传至github仓库:https://github.com/Q1mi/demo,请点击查看完整代码。