type
status
category
date
slug
summary
tags
password
icon
😀
这里写文章的前言: Protobuf全称Protocol Buffer,是 Google 公司于2008年开源的一种语言无关、平台无关、可扩展的用于序列化结构化数据——类似于XML,但比XML更小、更快、更简单,它可用于(数据)通信协议、数据存储等。你只需要定义一次你想要的数据结构,然后你就可以使用特殊生成的源代码来轻松地从各种数据流和各种语言中写入和读取你的结构化数据。目前 Protobuf 被广泛用作微服务中的通信协议。

📝 主旨内容

protobuf 编译器指南

安装 protobuf

从官方仓库:https://github.com/google/protobuf/releases 下载适合你平台的预编译好的二进制文件(protoc-<version>-<platform>.zip)。
其中:
  • bin 目录下的 protoc 是可执行文件。
  • include 目录下的是 google 定义的.proto文件,我们import "google/protobuf/timestamp.proto"就是从此处导入。
我们需要将下载得到的可执行文件protoc所在的 bin 目录加到我们电脑的环境变量中。

生成Go代码

编译器调用

protocol buffer编译器需要一个插件来根据提供的proto文件生成 Go 代码,Go1.16+请使用下面的命令安装插件。
这个命令将在 $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 而创建的。
在调用protoc时,通过传递 go_opt 标志来提供特定于 protocol-gen-go 的标志位参数。可以传递多个go_opt标志位参数。例如,当执行下面的命令时:
编译器将从 src 目录中读取输入文件 foo.proto 和 bar/baz.proto,并将输出文件 foo.pb.go 和 bar/baz.pb.go 写入 out 目录。如果需要,编译器会自动创建嵌套的输出子目录,但不会创建输出目录本身。
💡
参数--proto_path可以使用-I同等效果

package

为了生成 Go 代码,必须为每个 .proto 文件(包括那些被生成的 .proto 文件传递依赖的文件)提供 Go 包的导入路径。有两种方法可以指定 Go 导入路径:
  • 通过在 .proto 文件中声明它。
  • 通过在调用 protoc 时在命令行上声明它。
我们建议在 .proto 文件中声明它,以便 .proto 文件的 Go 包可以与 .proto 文件本身集中标识,并简化调用 protoc 时传递的标志集。 如果给定 .proto 文件的 Go 导入路径由 .proto 文件本身和命令行提供,则后者优先于前者。
Go 导入路径是在 .proto 文件中指定的,通过声明带有 Go 包的完整导入路径的 go_package 选项来创建 proto 文件。用法示例:
调用编译器时,可以在命令行上指定 Go 导入路径,方法是传递一个或多个 M${PROTO_FILE}=${GO_IMPORT_PATH} 标志位。用法示例:
由于所有 .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文件。
我们在这个文件中使用option go_package = "github.com/Q1mi/demo/proto/book"语句声明了生成的Go代码的导入路径。
项目当前的目录结构如下:

生成代码

假设我们想把最终生成的Go代码还保存在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,上述编译命令也可以这样写。
执行上述命令将会在proto目录下生成book/price.pb.go文件。
此处如果不指定--proto_path参数那么编译命令可以简写为:
上面的命令都是将代码生成到demo/proto目录,如果想要将生成的Go代码保存在其他文件夹中(例如pb文件夹),那么我们需要先在demo目录下创建一个pb文件夹。然后在命令行通过--go_out=pb指定生成的Go代码保存的路径。完整命令如下:
执行上面的命令便会在demo/pb文件夹下生成Go代码。

import同目录下protobuf文件

随着业务的复杂度上升,我们可能会定义多个.proto源文件,然后根据需要引入其他的protobuf文件。
在这个示例中,我们在demo/proto/book目录下新建一个book.proto文件,它通过import "book/price.proto";语句引用了同目录下的price.proto文件。
编译命令如下:
这里有几点需要注意:
  1. 因为我们通过编译命令指定-proto_path=proto,所以import导入语句需要从demo/proto文件夹的下层目录book这一层开始写。
  1. 因为导入的price.protobook.proto同属于一个package book;,所以可以直接使用price作为类型,无需添加 package 前缀(即无需写成book.price)。
上述编译命令最终会生成demo/proto/book/book.pb.go文件。

import其他目录下文件

我们在demo/proto目录下新建了一个author文件夹,用来存放与 author 相关的 protobuf 文件。例如我们定义一个表示作者信息的author.proto文件,其内容如下:
此时的目录结构:
假设我们的 book 需要增加一个作者信息的字段——authorInfo,这时我们需要在demo/proto/book/book.proto中导入其他目录下的 author.proto 文件。具体改动如下。
我们通过import "author/author.proto";导入了author包的author.proto文件,所以在book包下使用Info类型时需要添加其包名前缀即author.Info
编译命令如下:
此时的目录结构:

import google proto文件

有时候我们也需要在我们定义的 protobuf 文件中使用 Google 定义的类型,例如TimestampAny等。
例如我们要为我们的 book 添加出版日期——date字段,就可以通过 import "google/protobuf/timestamp.proto";导入并使用Timestamp类型了。
修改后的book.proto文件内容如下:
那么这个 google/protobuf/timestamp.proto 是从哪里导入的呢?
可以简单粗暴的把下载好的 protobuf 文件拷贝到你项目的 proto 目录下。
然后执行下面的编译命令:

生成gRPC代码

由于通常我们都是配合 gRPC 来使用 protobuf ,所以我们也需要基于.proto文件生成Go代码的同时生成 gRPC 代码。
要想生成 gRPC 代码就需要先安装 protoc-gen-go-grpc 插件。
上述命令会默认将插件安装到$GOPATH/bin,为了protoc编译器能找到这些插件,请确保你的$GOPATH/bin在环境变量中。
假设我们现在要提供一个创建书籍的 RPC 方法,那么我在book.proto中添加如下定义。
然后在 protoc 的编译命令添加 gRPC相关输出的参数,完整命令如下。
上述命令就会生成book_grpc.pb.go文件。

gRPC-Gateway

gRPC-Gateway 也是日常开发中比较常用的一个工具,`它同样也是根据 protobuf 生成相应的代码。

安装工具

为protobuf文件添加注释

我们在book.proto文件中添加如下注释。
此时,我们又引入了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文件)。
此时的项目目录如下:

编译

这一次编译命令在之前的基础上要继续加上 gRPC-Gateway相关的 --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative 参数。
完整的编译命令如下:
💡
--go_opt=paths=source_relative,这个参数可以使得我们gen的文件和.proto文件保存位置相对,很好用的参数
最终会编译得到一个book.pb.gw.go文件。
为了方便编译可以在项目下定义Makefile
后续想要编译只需在项目目录下执行make gen即可。
windows下可以换成

管理 protobuf

在企业的项目开发中,我们通常会把 protobuf 文件存储到一个单独的代码库中,并在具体项目中通过git submodule引入。这样做的好处是能够将 protobuf 文件统一管理和维护,避免因 protobuf 文件改动导致的问题。
 
 
Go RPC解决Makefile文件内容打印出来会乱码