一文了解protoc的使用

前言

前面我们提到,gRPC使用protobuf进行序列化和反序列化,同时也将其作为接口定义语言,而protobuf的核心之一,莫过于protoc工具的使用,今天,就让我带你踩踩protoc的坑!

注:本文使用的protoc版本为 3.7.1

安装

# 安装
$ brew install protoc

# 查看版本
$ protoc --version
libprotoc 3.7.1
复制代码

基础篇

基础篇主要是让大家熟悉protoc的一些基本参数,以及常见用法,项目结构如下:

├── proto1
│   ├── greeter
│   │   ├── greeter.proto
│   │   └── greeter_v2.proto
│   └── pb_go
复制代码

greeter.proto文件内容:

syntax = "proto3";

package greeter;

option go_package="proto1/greeter";

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
    string name = 1;
}
message HelloReply {
    string message = 1;
}
复制代码

编译命令如下:

protoc --proto_path=. --go_out=. proto1/greeter/greeter.proto

上面的指令可以拆解为三部分,分别对应protoc的三个重要参数,我们首先来看看protoc提供了哪些参数:

$ protoc --help
Usage: protoc [OPTION] PROTO_FILES

  -IPATH, --proto_path=PATH   指定搜索路径
  --plugin=EXECUTABLE:
  
  ....
 
  --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.
  --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
  
   @<filename>                proto文件的具体位置
复制代码

1.搜索路径参数

第一个比较重要的参数就是搜索路径参数,即上述展示的-IPATH, --proto_path=PATH。它表示的是我们要在哪个路径下搜索.proto文件,这个参数既可以用-I指定,也可以使用--proto_path=指定。

如果不指定该参数,则默认在当前路径下进行搜索;另外,该参数也可以指定多次,这也意味着我们可以指定多个路径进行搜索。

比如下面这三条指令,表达的含义都是一样的

# Makefile
GOPATH:=$(shell go env GOPATH)

# proto1 相关指令
.PHONY: proto1
path1:
	protoc --go_out=. proto1/greeter/greeter.proto
path2:
	protoc -I. --go_out=. proto1/greeter/greeter.proto # 点号表示当前路径,注意-I参数没有等于号
path3:
	protoc --proto_path=. --go_out=. proto1/greeter/greeter.proto
复制代码

注:protoc的编译命令我会比较习惯写在Makefile中,一是容易查找,二是执行起来简单。

2.语言插件参数

语言参数即上述的--cpp_out=--python_out=等,protoc支持的语言长达13种,且都是比较常见的。像上面出现的语言参数,说明protoc本身已经内置该语言对应的编译插件,我们无需安装。

而如果上面没出现的,比如--go_out=,就得自己单独安装语言插件,比如--go_out=对应的是protoc-gen-go,安装命令如下:

# 最新版
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

# 指定版本
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.3.0
复制代码

如果想了解更多语言的编译插件,可点击protobuf官方文档进行查阅。

3.proto文件位置参数

proto文件位置参数即上述的@<filename>参数,指定了我们proto文件的具体位置,如proto1/greeter/greeter.proto

进阶篇

两个易混参数

proto文件(非protoc)有两个易混参数,即packagexx_package,xx指的是你的编译语言,比如你要编程成Go语言,对应的就是go_package

package

package参数针对的是protobuf,是proto文件的命名空间,它的作用是为了避免我们定义的接口,或者message出现冲突。

举一个小栗子,假设我有A.protoB.proto两份文件,如下

# A.proto
message UserInfo {
    uint32 uid = 1;
    string name = 2;
}

# B.proto
message UserInfo {
    uint32 uid = 1;
    string name = 2;
    uint32 age = 3;
    string work = 4;
}
复制代码

如上,两份文件同时有一个UserInfo的message,这时候如果我需要在A文件引用B文件,如果没有指定package,就无法区分是要调A的UserInfo还是调B的。

xx_package

这里以go_package进行举例说明,该参数主要声明Go代码的存放位置,也可以说它解决的是包名问题(因为proto文件编译后会生成一份.pb.go文件,既然是go文件,就有包名问题)

.pb.go常规的存放路径一般是放在同名proto文件下,但也有些人不想这么做,比如他想把所有.pb.go文件都存放在一个特定文件夹下,比如上述的 pb_go,那么他有两种办法:

第一种:

# 修改 --go_out,go_package 保持不变
$ protoc --proto_path=. --go_out=./proto1/pb_go proto1/greeter/greeter.proto

这样生成的pb文件在 pb_go/proto1/greeter 目录下,文件目录有点冗余,不过pb文件的包名仍然是 greeter
复制代码

第二种:

# 修改 go_package, go_out 保持不变
option go_package="proto1/pb_go";

$ protoc --proto_path=. --go_out=. proto1/greeter/greeter_v2.proto

这样生成的pb文件在 pb_go 目录下,pb文件的包名为 pb_go
复制代码

另外,xx_package其实有两种声明方式,第一种是在文件中声明,你已经见过了;第二种是在命令行中声明,声明格式如下:M${PROTO_FILE}=${GO_IMPORT_PATH}

目前,官方文档比较推荐在文件中声明,目的是可以缩短protoc指令的长度,我个人也比较推荐这种方式。

好了,相信通过以上几个例子,你已经能大致弄懂packagexx_package的区别了,再次强调下,官方文档中已经说明,packagexx_package两者没有关联,属于不同范畴。

--go_out详细解读

想必大家在使用的时候,应该遇到过这些写法:--go_out=paths=import:.--go_out=paths=source_relative:.,或者--go_out=plugins=grpc:.

这样写表达的是啥意思呢?

所以我们需要知道,--go_out参数是用来指定 protoc-gen-go 插件的工作方式和Go代码的生成位置,而上面的写法正是表明该插件的工作方式。

--go_out主要的两个参数为pluginspaths,分别表示生成Go代码所使用的插件,以及生成的Go代码的位置。--go_out的写法是,参数之间用逗号隔开,最后加上冒号来指定代码的生成位置,比如--go_out=plugins=grpc,paths=import:.

paths参数有两个选项,分别是 importsource_relative,默认为 import,表示按照生成的Go代码的包的全路径去创建目录层级,source_relative 表示按照 proto源文件的目录层级去创建Go代码的目录层级,如果目录已存在则不用创建。

举例说明:

option go_package="proto1/pb_go";

# 指令1:paths为import,pb文件最终在 pb_go 目录下
$ protoc --proto_path=. --go_out=. proto1/greeter/greeter_v2.proto
$ protoc --proto_path=. --go_out=paths=import:. proto1/greeter/greeter_v2.proto

# 指令2:paths为source_relative,pb文件最终在 proto1/greeter 目录下
$ protoc --proto_path=. --go_out=paths=source_relative:. proto1/greeter/greeter_v2.proto
复制代码

plugins参数有不带grpc和带grpc两种(应该还有其它的,目前知道的有这两种),两者的区别如下,带grpc的会多一些跟gRPC相关的代码,实现gRPC通信:

image.png

导入其它proto文件

有时候我们可能有多份proto文件,且每份proto文件不一定是完全独立,它们之间会互相引用,这时候该怎么做呢?

└── proto2
    ├── common.proto
    └── greeter
        └── greeter.proto
复制代码

greeter.proto内容如下:

syntax = "proto3";

package greeter;

import "proto2/common.proto";

option go_package="proto2/greeter";

service Greeter {
    rpc SayHello (common.Request) returns (common.Response) {}
}
复制代码

以上述内容为例,假设我们有一个共用的 proto 文件 common.proto,此时 greeter.proto 如果想引用里面的message,就可以使用 import 关键字进行导入。

编译指令如下:

protoc --proto_path=. --go_out=. proto2/greeter/greeter.proto proto2/common.proto

关于protoc-gen-go的多包问题

假设目前的proto文件存放如下:

└── proto
    ├── common.proto
    ├── greeter
    │   └── greeter.proto
    └── user
        └── user.proto
复制代码

如果你想编译所有proto文件(假设生成Go语言),正常的命令应该是这样的:

protoc --proto_path=. --go_out=. proto/*.proto proto/user/*proto proto/greeter/*proto

但是有的朋友可能会想偷懒,想直接这样:

protoc --proto_path=. --go_out=. proto/*.proto

答案是不行的,因为protoc-gen-go不支持这种形式,最终只会编译common.proto

写在最后

本文主要讲述了protoc的基本用法,并举了几个protoc经常遇到的问题,这些问题主要是围绕pb文件的生成位置,以及包名如何定义,建议还是要多动手,多实践。本文源代码可以在本人的Github上查阅

本文来来回回改了好几次,写到手疼,如果觉得有用的话,麻烦为我点个赞!

我是言淦,欢迎关注我的公众号言淦Coding,你们的点赞与关注是我创作的最大动力!!

参考文档

Protobuf 的 import 功能在 Go 项目中的实践