生成 Go 代码

为了保持工具的中立性,我将介绍三种不同的工具,从最低层次到最高层次。我们将首先看到如何使用 protoc 手动生成代码。然后,考虑到我们不想每次都编写冗长的命令行,我们将介绍如何使用 Buf 简化代码生成。最后,我们将看到如何使用 Bazel 将代码生成集成到构建过程中。

在这一部分,我将展示如何编译 proto 文件的基本方法。大多数情况下,这些命令可以满足需求,但有时您可能需要查阅每个工具的文档。 对于 protoc,您可以运行 protoc --help 获取选项列表。 对于 Buf,您可以访问在线文档: Buf 文档。 对于 Bazel,您也可以访问在线文档: Bazel 文档

Protoc

使用 protoc 是从 proto 文件手动生成代码的方法。如果你只处理少量的 proto 文件,并且文件之间没有很多依赖(如导入),这种方法可能还可以接受。否则,随着项目的复杂性增加,手动使用 protoc 会变得非常麻烦,正如我们将看到的那样。

不过,我认为我们应该先了解一下 protoc 命令,这样我们可以对它的功能有一个基本的了解。而且,许多高级工具都是基于 protoc 构建的,因此这将帮助我们理解它的不同特性。

使用前面提到的 dummy.proto 文件,我们可以在根目录(chapter4 文件夹)运行如下命令:

$ protoc --go_out=. \
    --go_opt=module=github.com/PacktPublishing/gRPC-Go-for-Professionals \
    --go-grpc_out=. \
    --go-grpc_opt=module=github.com/PacktPublishing/gRPC-Go-for-Professionals \
    proto/dummy/v1/dummy.proto

这看起来可能有点复杂,实际上这不是你可以编写的最简洁的命令。我将在介绍 Buf 时向你展示一个更简洁的命令。但首先,让我们将上述命令分解成几个部分来理解。

在讨论 --go_out--go-grpc_out 之前,我们先来看一下 --go_opt=module--go-grpc_opt=module 这两个选项。这些选项用于告诉 protoc 需要根据在 Proto 文件中传递的 go_package 选项来剥离公共模块。假设我们有如下内容:

option go_package = "github.com/PacktPublishing/gRPC-Go-for-Professionals/proto/dummy/v1";

然后,--go_opt=module=github.com/PacktPublishing/gRPC-Go-for-Professionals 会从 go_package 中剥离出 module= 后面的部分,这样最终得到的就是 /proto/dummy/v1

现在我们理解了这一点,我们可以讨论 --go_out--go-grpc_out 这两个选项。这两个选项告诉 protoc 生成 Go 代码的位置。在我们的例子中,看起来我们是在告诉 protoc 在根目录生成代码,但实际上,因为它与之前提到的两个选项结合使用,它会把代码生成到 Proto 文件所在的目录旁边。这是由于去除包路径的操作,导致 protoc 会在 /proto/dummy/v1 包路径下生成代码。

现在,您可以看到每次编写这样的命令可能会有多么痛苦。大多数人不会这么做。他们通常会编写脚本来自动执行此操作,或者使用其他工具,如 Buf

Buf

对于 Buf,我们需要进行一些设置来生成代码。在项目的根目录(chapter4)下,我们将创建一个 Buf 模块。为此,我们只需要运行以下命令:

$ buf mod init

这将创建一个名为 buf.yaml 的文件。这个文件是用来设置项目级别的选项的,比如代码检查或跟踪破坏性更改。虽然这些内容超出了本书的范围,但如果你对这个工具感兴趣,可以查看官方文档: Buf Getting Started

创建完 buf.yaml 后,我们需要写一个代码生成的配置。在一个名为 buf.gen.yaml 的文件中,我们将配置如下内容:

version: v1
plugins:
  - plugin: go
    out: proto
    opt: paths=source_relative
  - plugin: go-grpc
    out: proto
    opt: paths=source_relative

在这里,我们定义了使用 Go 插件来生成 Protobuf 和 gRPC 代码。对于每个插件,我们都指定了代码生成的输出目录为 proto,并且使用了一个 --go_opt--go-grpc_opt 的配置项 paths=source_relative。当设置为 paths=source_relative 时,生成的代码会放在与输入文件(如 dummy.proto)相同的目录下。最终,Buf 执行的命令类似于我们在 protoc 部分执行的命令:

$ protoc --go_out=. \
  --go_opt=paths=source_relative \
  --go-grpc_out=. \
  --go-grpc_opt=paths=source_relative \
  proto/dummy/v1/dummy.proto

为了使用 Buf 运行代码生成,我们只需要在 chapter4 目录下运行以下命令:

$ buf generate proto

对于中型或大型项目,使用 Buf 是很常见的做法。它帮助自动化代码生成,并且很容易上手。不过,你可能已经注意到,我们需要先生成代码,然后再构建 Go 应用程序。接下来,Bazel 将帮助我们将这一切合并到一步中。

Bazel

在本节中,我将使用名为 GO_VERSIONRULES_GO_VERSIONRULES_GO_SHA256GAZELLE_VERSIONGAZELLE_SHA256PROTO_VERSION 的变量。我们没有在本节中包含这些变量,以确保本书能够轻松更新。你可以在 chapter4 文件夹中的 versions.bzl 文件中找到这些版本信息( GitHub 链接 )。

Bazel 的设置稍微复杂一些,但它值得付出努力。一旦你的构建系统搭建完成,你将能够通过一个命令完成整个应用程序的构建(包括代码生成和构建)或运行。

Bazel 中,我们从定义一个名为 WORKSPACE.bazel 的文件开始,文件位于项目的根目录。在这个文件中,我们定义项目的所有依赖项。在我们的例子中,我们有 Protobuf 和 Go 的依赖。此外,我们还将添加一个对 Gazelle 的依赖,Gazelle 将帮助我们创建生成代码所需的 BUILD.bazel 文件。

所以,在 WORKSPACE.bazel 文件中,在其他内容之前,我们将定义我们的工作区名称,导入我们的版本变量,并导入一些实用工具来克隆 Git 仓库和下载归档文件:

workspace(name = "github_com_packtpublishing_grpc_go_for_professionals")
load("//:versions.bzl",
  "GO_VERSION",
  "RULES_GO_VERSION",
  "RULES_GO_SHA256",
  "GAZELLE_VERSION",
  "GAZELLE_SHA256",
  "PROTO_VERSION"
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

接着,我们添加 Gazelle 依赖项:

http_archive(
  name = "bazel_gazelle",
  sha256 = GAZELLE_SHA256,
  urls = [
    "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/%s/bazel-gazelle-%s.tar.gz" % (GAZELLE_VERSION, GAZELLE_VERSION),
    "https://github.com/bazelbuild/bazel-gazelle/releases/download/%s/bazel-gazelle-%s.tar.gz" % (GAZELLE_VERSION, GAZELLE_VERSION),
  ],
)

然后,我们添加 Go 构建工具的依赖项:

http_archive(
  name = "io_bazel_rules_go",
  sha256 = RULES_GO_SHA256,
  urls = [
    "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/%s/rules_go-%s.zip" % (RULES_GO_VERSION, RULES_GO_VERSION),
    "https://github.com/bazelbuild/rules_go/releases/download/%s/rules_go-%s.zip" % (RULES_GO_VERSION, RULES_GO_VERSION),
  ],
)

现在我们有了这些,我们可以拉取 rules_go 的依赖项,设置构建 Go 项目的工具链,并告诉 Gazelle 在哪里找到我们的 WORKSPACE.bazel 文件:

load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")

go_rules_dependencies()

go_register_toolchains(version = GO_VERSION)

gazelle_dependencies(go_repository_default_config = "//:WORKSPACE.bazel")

然后,我们拉取 Protobuf 的依赖并加载它的依赖项:

git_repository(
  name = "com_google_protobuf",
  tag = PROTO_VERSION,
  remote = "https://github.com/protocolbuffers/protobuf"
)

load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")

protobuf_deps()

我们的 WORKSPACE.bazel 文件就完成了。

接下来,我们来配置根目录下的 BUILD.bazel 文件。在这个文件中,我们将定义运行 Gazelle 的命令,并告诉 Gazelle 我们的 Go 模块名称,同时要求它忽略 proto 目录中的 Go 文件。如果不这样做,Gazelle 会认为 proto 目录中的 Go 文件也应该有自己的 Bazel 目标文件,这可能会带来一些问题:

load("@bazel_gazelle//:def.bzl", "gazelle")

# gazelle:exclude proto/**/*.go
# gazelle:prefix github.com/PacktPublishing/gRPC-Go-for-Professionals
gazelle(name = "gazelle")

有了这些,我们现在可以运行以下命令:

$ bazel run //:gazelle

如果你通过 Bazelisk 安装了 Bazel,每次运行 Bazel 命令时,Bazel 都会尝试获取其最新版本。为了避免这种情况,你可以创建一个名为 .bazelversion 的文件,里面包含你当前安装的 Bazel 版本。你可以通过输入 bazel --version 来查看版本。

可以在 chapter4 文件夹中找到一个示例。

在依赖项被拉取并编译后,你应该能够看到在 proto/dummy/v1 目录下生成一个 BUILD.bazel 文件。这个文件中最重要的部分是以下的 go_library

go_library(
  name = "dummy",
  embed = [":v1_go_proto"],
  importpath = "github.com/PacktPublishing/gRPC-Go-for-Professionals/proto/dummy/v1",
  visibility = ["//visibility:public"],
)

稍后,我们将使用这个库并将其链接到我们的二进制文件中。它包含了我们开始使用所需的所有生成代码。