前面说过,grpc使用Protocol Buffer(简称protobuf)作为接口描述语言,protobuf的重点之一即是protoc工具的使用,这篇文章将介绍protoc工具的使用,特别是和go相关插件的组合使用。
我们首先构建一个项目,项目结构如下:
proto-project
├── cient.go
├── genproto
├── proto
│ ├── messagepb
│ └── simplepb
│ └── simple.proto
└── server.go
其中,simple.proto的内容如下:
syntax = "proto3";
package simplepb;
option go_package = "github.com/IguoChan/proto-project/genproto/simplepb";
message SimpleRequest{
string data = 1;
}
message SimpleResponse{
int32 code = 1;
string value = 2;
}
// 定义我们的服务(可定义多个服务,每个服务可定义多个接口)
service Simple{
rpc Route (SimpleRequest) returns (SimpleResponse){};
}
我们希望最后生成的go文件出现在genproto文件夹中,我们在项目根目录下执行以下指令:
protoc -I./proto --go_out=./genproto --go_opt paths=source_relative ./proto/*/*.proto
从上面可以看出,protoc命令有三个重要的参数,分别是搜索路径参数、语言插件参数和文件位置参数,以下将分别介绍一下这三种参数。
2.1 搜索路径参数
$ protoc --help
Usage: protoc [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
-IPATH, --proto_path=PATH Specify the directory in which to search for
imports. May be specified multiple times;
directories will be searched in order. If not
given, the current working directory is used.
If not found in any of the these directories,
the --descriptor_set_in descriptors will be
checked for required proto file.
我们通过这个参数来设置需要导入的proto文件位置,如果没设置,那就以当前工作目录为准去寻找,通常写作-IPATH
或者--proto_path=PATH
。
2.2 文件位置参数
最后就是文件位置参数,文件位置参数的描述如下:
@<filename> Read options and filenames from file. If a
relative file path is specified, the file
will be searched in the working directory.
The --proto_path option will not affect how
this argument file is searched. Content of
the file will be expanded in the position of
@<filename> as in the argument list. Note
that shell expansion is not applied to the
content of the file (i.e., you cannot use
quotes, wildcards, escapes, commands, etc.).
Each line corresponds to a single argument,
even if it contains spaces.
按照其中的描述,proto_path,也就是搜索路径参数对其并无影响,但是实际上并不是的,比如我们在messagepb文件夹新增一个message.proto文件,其引用了simple.proto,如下:
syntax = "proto3";
package messagepb;
option go_package = "github.com/IguoChan/proto-project/genproto/messagepb";
import "simplepb/simple.proto";
message MessageRequest {
simplepb.SimpleRequest req = 1;
}
在项目的根目录下,执行以下指令,可以生成正确的go代码:
$ pwd
/Users/xxx/workspace/proto-project
$ protoc -I./proto --go_out=./genproto --go_opt paths=source_relative ./proto/messagepb/message.proto
此时整个项目如下,说明message.pb.go文件生成的是正确的.
proto-project
├── client.go
├── genproto
│ └── messagepb
│ └── message.pb.go
├── go.mod
├── go.sum
├── proto
│ ├── messagepb
│ │ └── message.proto
│ └── simplepb
│ └── simple.proto
└── server.go
但是如果我们到以下目录执行如下指令时,就会发现会报错:
$ pwd
/Users/xxx/workspace/proto-project/proto/messagepb
$ protoc -I../ --go_out=../../genproto --go_opt paths=source_relative ./message.proto
./message.proto: File does not reside within any path specified using --proto_path (or -I). You must specify a --proto_path which encompasses this file. Note that the proto_path must be an exact prefix of the .proto file names -- protoc is too dumb to figure out when two paths (e.g. absolute and relative) are equivalent (it's harder than you think).
最开始时,我并不理解这两种方式有什么区别,通过万能的互联网也没有找到特别让人信服的说法,比如以下两种:
$ protoc -I../ --go_out=../../genproto --go_opt paths=source_relative ./messagepb/message.proto
Could not make proto path relative: ./messagepb/message.proto: No such file or directory
$ pwd
/Users/xxx/workspace/proto-project/proto/messagepb
# 1.文件位置参数中必须包含-I中的参数
$ protoc -I../ --go_out=../../genproto --go_opt paths=source_relative ../messagepb/message.proto
# 2.绝对路径时也必须满足“文件位置参数中必须包含-I中的参数”
protoc -I/Users/xxx/workspace/proto-project/genproto/../proto/messagepb/../ --go_out=/Users/xxx/workspace/proto-project/proto/messagepb/../../genproto --go_opt paths=source_relative /Users/xxx/workspace/proto-project/genproto/../proto/messagepb/../messagepb/message.proto
# 3.以下这种形式也可以
protoc -I../ --go_out=../../genproto --go_opt paths=source_relative messagepb/message.proto
实验总结有以下规律,但是没有从其他资料得到验证(也没有时间去扒拉源码啦),如果有问题还请大家指正!
多个-I参数时,有满足以上条件的即可。至于protoc --help中给出的-I参数对文件位置参数不影响,我觉得描述有误。
2.3 语言插件参数
protoc自带了一些语言插件,如下:
--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.
--kotlin_out=OUT_DIR Generate Kotlin file.
--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.
但是其并不包括go的语言插件,所以我们需要自己安装,Go Generated Code。其实安装教程我们在前一章已经描述过了,下面重点讲一下文件的输出位置。
我们执行以下指令,可以在genproto文件夹下生成了我们希望的文件输出位置。
$ pwd
/Users/xxx/workspace/proto-project
$ protoc -I./proto --go_out=./genproto --go_opt paths=source_relative ./proto/*/*.proto
$ tree genproto
genproto
├── messagepb
│ └── message.pb.go
└── simplepb
└── simple.pb.go
那么如何控制生成go文件的输出位置呢?官方文档中有描述,protoc通过–go_out参数和–go_opt参数共同决定:
If the
paths=import
flag is specified, the output file is placed in a directory named after the Go package’s import path. For example, an input fileprotos/buzz.proto
with a Go import path ofexample.com/project/protos/fizz
results in an output file atexample.com/project/protos/fizz/buzz.pb.go.
This is the default output mode if apaths
flag is not specified.
If thepaths=source_relative
flag is specified, the output file is placed in the same relative directory as the input file. For example, an input fileprotos/buzz.proto
results in an output file atprotos/buzz.pb.go.
paths默认为import,在这种模式下,其会在–go_out所指定的目录下完全按照go package(下章介绍)的路径生成文件,比如执行以下命令:
$ pwd
/Users/xxx/workspace/proto-project
$ protoc -I./proto --go_out=./genproto ./proto/*/*.proto
$ tree genproto
genproto
└── github.com
└── IguoChan
└── proto-project
└── genproto
├── messagepb
│ └── message.pb.go
└── simplepb
└── simple.pb.go
可以发现,其在genproto文件夹下生成了完全按照.proto文件中option go_package所示的文件路径的格式。说实话,这种方式对于工程应用很不友好,比如正常工程的go package地址常带着git仓库的头,而git仓库并不是工程名的一部分,导致很不好管理,所以我们一般使用relative的生成方式。
当使用–go_opt paths=source_relative指定生成的文件位置时,其生成规则是.proto文件相对于-I参数的相对位置,即是生成的.pb.go文件相对–go_out参数的相对位置,所以以下指令才能生成如下的工程文件。
$ pwd
/Users/xxx/workspace/proto-project
$ protoc -I./proto --go_out=./genproto --go_opt paths=source_relative ./proto/*/*.proto
$ tree ../proto-project
../proto-project
├── client.go
├── genproto
│ ├── messagepb
│ │ └── message.pb.go
│ └── simplepb
│ └── simple.pb.go
├── go.mod
├── go.sum
├── proto
│ ├── messagepb
│ │ └── message.proto
│ └── simplepb
│ └── simple.proto
└── server.go
Packages,为了生成go代码,go包是必须要指定的,在protoc中,有两种方式指定go的包:
官方建议在.proto文件中声明它,以便文件的Go包可以用.proto文件本身集中标识,并简化调用时传递的标志集protoc。
需要明确的是,.proto文件中的package和指定go包的go_package没有任何关系。
package参数针对的是protobuf,是proto文件的命名空间,它的作用是为了避免我们定义的接口,或者message出现冲突。
go_package参数主要声明Go代码的存放位置,也可以说它解决的是包名问题。