Buf CLI 快速入门

Buf CLI 将取代 protoc 成为您处理 Protobuf 文件的主要工具:编译模式、代码检查、检测破坏性变更以及生成代码。在本快速入门中,您将学习:

  1. 使用 buf.yaml 文件配置工作区和模块。
  2. 生成代码桩,以便与您的 API 集成。
  3. 对 Protobuf 文件进行代码检查,捕获错误并确保它们遵循最佳实践。
  4. 检测破坏性变更以保持向后兼容性。
  5. 实现一个使用生成代码的服务器并调用您的 API。

前提条件

  • 如果尚未安装,请安装 Buf CLI。本教程需要 1.32.0 或更高版本,如果您之前安装过 Buf CLI,请检查版本并在必要时更新:
    $ buf --version
    
  • 确保已安装 gitgo,并将它们添加到您的 $PATH 中。
  • 克隆 buf-examples 仓库并进入本快速入门的目录:
git clone git@github.com:bufbuild/buf-examples.git
cd buf-examples/cli/quickstart/start/

快速入门包含一个 start 目录(您将在其中处理示例文件)和一个 finish 目录(可用于对照比较)。

配置与构建

首先配置 Buf CLI 并构建定义宠物商店 API 的 .proto 文件,该 API 规定了在商店中创建、获取和删除宠物的方式。

配置工作区

您可以使用 buf.yaml 文件配置 Buf CLI 工作区,该文件定义了您希望视为逻辑单元(即模块)的 Protobuf 文件目录列表。使用以下命令创建该文件:

buf config init

运行命令后,工作区目录中会出现一个 buf.yaml,内容如下:

# 有关 buf.yaml 配置的详细信息,请访问 https://buf.build/docs/configuration/v2/buf-yaml
version: v2
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

buf.yaml 文件位于工作区的根目录,它所定义的工作区是所有 Buf 操作的默认输入

更新目录路径并构建模块

生成的 buf.yaml 文件默认使用一个根目录为 .(当前目录)的模块。由于您的 .proto 文件位于 proto/ 子目录中,您需要显式设置模块路径。这样可以限制 Buf 操作仅针对 Protobuf 文件,并将生成的配置和 Go 代码排除在模块根目录之外。使用 modules 键将 proto 目录添加到 buf.yaml 文件中:

version: v2
+modules:
+  - path: proto
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

在继续之前,请验证所有设置是否正确并且模块可以构建。如果没有错误,说明您已正确配置 Buf 模块:

buf build

如果命令没有输出任何内容,则模块配置正确。

生成代码

Buf CLI 提供了用户友好的本地代码生成体验,且与 protoc 兼容。接下来您将生成一些代码。

配置 buf.gen.yaml 文件

配置好模块后,下一步是创建 buf.gen.yaml 文件来配置本地代码生成。该文件控制 buf generate 命令如何在给定模块上执行 protoc 插件。您可以用它来配置每个 protoc 插件写入结果的位置,并为每个插件指定选项。

在当前目录中创建 buf.gen.yaml 文件,内容如下,以使用 Go 和 Connect-Go 插件生成代码:

version: v2
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: github.com/bufbuild/buf-examples/gen
plugins:
  - remote: buf.build/protocolbuffers/go
    out: gen
    opt: paths=source_relative
  - remote: buf.build/connectrpc/gosimple
    out: gen
    opt:
      - paths=source_relative
      - simple
inputs:
  - directory: proto

根据此配置,Buf CLI 会做两件事:

  • 执行 protocolbuffers/go 插件,为您的 .proto 文件生成 Go 特定代码,并将其输出放在 gen 目录中。
  • 执行 connectrpc/gosimple 插件,为 Connect-Go 生成客户端和服务器桩代码到 gen 目录。

此配置中有几点需要注意:

  • 托管模式 (Managed mode): 托管模式 是一种配置选项,它告诉 Buf CLI 根据一套针对所支持的 Protobuf 语言而设定的意见性值,自动设置您工作区中的所有文件选项。文件选项长期以来一直是 Protobuf 用户困惑和挫败的根源,因此托管模式会根据配置动态设置它们,允许您从 .proto 文件中删除它们。
  • 远程插件 (Remote plugins): 此处指定的插件是托管在 Buf Schema Registry 上的远程插件。使用它们无需在本地机器上下载、维护或运行插件。
  • 输入 (Inputs): buf generate 命令可以接受多种类型的输入,例如 Buf 模块、GitHub 仓库和 tarball/zip 归档。示例代码指向工作区中的 proto 子目录。

生成 Go 和 Connect RPC 桩代码

现在您已配置好 buf.gen.yaml 文件,就可以生成与 PetStoreService API 关联的 Connect RPC 和 Go 代码。这些插件将从 Buf Schema Registry 获取。请注意,未经身份验证的请求会受到速率限制,因此如果您有 BSR 账户,可以登录以提高限制:

buf registry login

然后运行:

buf generate

如果成功,您会注意到 gen 目录中出现了一些新文件 — 它们就是生成的代码桩:

.
├── gen
│   ├── google
│   │   └── type
│   │       └── datetime.pb.go
│   └── pet
│       └── v1
│           ├── pet.pb.go
│           └── petv1connect
│               └── pet.connect.go
├── proto
│   ├── google
│   │   └── type
│   │       └── datetime.proto
│   └── pet
│       └── v1
│           └── pet.proto
├── buf.gen.yaml
└── buf.yaml

这就是使用 Buf CLI 生成代码的简便方法。无需构建一组复杂的 protoc 命令 — 您的整个配置都包含在 buf.gen.yaml 文件中。

代码检查您的 Protobuf 文件

尽管 Buf CLI 是 protoc 的绝佳替代品,但它远不止是一个 Protobuf 编译器。它还通过 buf lint 命令提供代码检查功能。当您运行 buf lint 时,它会根据指定的代码检查规则集检查 buf.yaml 文件中列出的所有模块。

运行此命令以检查快速入门工作区中的所有 .proto 文件是否存在代码检查错误:

$ buf lint

proto/google/type/datetime.proto:17:1:Package name "google.type" should be suffixed with a correctly formed version, such as "google.type.v1".
proto/pet/v1/pet.proto:56:10:Field name "petID" should be lower_snake_case, such as "pet_id".
proto/pet/v1/pet.proto:61:9:Service name "PetStore" should be suffixed with "Service".

当前宠物商店 API 在两个文件中都存在一些代码检查失败的问题。这些失败违反了 buf.yaml 文件中配置的 STANDARD 类别中的规则。

修复代码检查失败

首先修复 pet/v1/pet.proto 文件中的代码检查失败,这些失败源于 FIELD_LOWER_SNAKE_CASESERVICE_SUFFIX 规则。结果精确指出了修复错误所需进行的更改,因此请更新 pet.proto 文件:

syntax = "proto3";

package pet.v1;

...

message DeletePetRequest {
-  string petID = 1;
+  string pet_id = 1;
}

message DeletePetResponse {}

-service PetStore {
+service PetStoreService {
  rpc GetPet(GetPetRequest) returns (GetPetResponse) {}
  rpc PutPet(PutPetRequest) returns (PutPetResponse) {}
  rpc DeletePet(DeletePetRequest) returns (DeletePetResponse) {}
}

再次运行 buf lint 以确认其中两个失败问题已解决:

$ buf lint

proto/google/type/datetime.proto:17:1:Package name "google.type" should be suffixed 
with a correctly formed version, such as "google.type.v1".

由于您更改了 petID 字段和服务名称,需要重新生成代码桩:

buf generate

忽略代码检查失败

google/type/datetime.proto 实际上不是您本地项目中的文件。相反,它是您的依赖项之一,由 googleapis 提供,因此您无法更改其 package 声明来修复代码检查失败。相反,您可以通过以下配置更改告诉 Buf CLI 忽略 google/type/datetime.proto 文件:

version: v2
modules:
  - path: proto
lint:
  use:
    - STANDARD
+  ignore:
+    - proto/google/type/datetime.proto
breaking:
  use:
    - FILE

最后再运行一次 buf lint,应该不再有错误。

检测破坏性变更

Buf CLI 还能让您检测 API 不同版本之间的破坏性变更。buf breaking 命令会根据指定的破坏性变更规则集检查 buf.yaml 文件中列出的所有模块,并与 Protobuf 模式的旧版本进行比较。这些规则是可选的,并根据您关心的破坏性变更类型分成逻辑类别

  • FILE:检测会导致生成代码在文件之间移动的变更,从而破坏基于文件的生成源代码。
  • PACKAGE:检测会破坏基于包的生成源代码变更的变更。它检测会破坏生成桩代码的变更,但仅考虑包级别的变更。
  • WIRE_JSON:检测会破坏 wire(二进制)或 JSON 编码的变更。由于 JSON 无处不在,我们建议将此作为最低级别。
  • WIRE:检测会破坏 wire(二进制)编码的变更。

默认值为 FILE,我们建议使用此值以保证 API 消费者之间的最大兼容性。我们建议仅选择其中一个选项,而不是像指定代码检查配置那样包含/排除特定的破坏性变更规则。您当前的 buf.yaml 文件已配置了 FILE 选项:

version: v2
modules:
  - path: proto
lint:
  use:
    - STANDARD
  ignore:
    - proto/google/type/datetime.proto
breaking:
  use:
    - FILE

破坏您的 API

要查看该功能的实际效果,您需要引入一个破坏性变更。首先,在 WIRE 级别进行一项破坏性变更。这是最基本的破坏性变更类型,因为它会改变 Protobuf 消息在传输(“on the wire”)中的编码方式。这种类型的破坏性变更会影响所有语言的所有用户

Pet.pet_type 字段的类型从 PetType 更改为 string

message Pet {
- PetType pet_type = 1;
+ string pet_type = 1;
  string pet_id = 2;
  string name = 3;
}

运行 buf breaking

现在,通过运行 buf breaking 来验证这是一个破坏性变更,方法是选择一个输入与您的工作区进行比较。在本例中,您将与本地的 main Git 分支进行比较。路径后的 #key=value 片段语法是 Buf 指定输入修饰符的方式:branch=main 选择 Git 分支,subdir=cli/quickstart/start/proto 将比较范围限制在该分支内的 proto 子目录中。

# --against 输入指向克隆仓库 main 分支中的 'proto' 子目录。
# '../../../' 从 cli/quickstart/start/ 返回到仓库根目录。
$ buf breaking --against "../../../.git#branch=main,subdir=cli/quickstart/start/proto"

proto/pet/v1/pet.proto:1:1:Previously present service "PetStore" was deleted from file.
proto/pet/v1/pet.proto:32:3:Field "1" with name "pet_type" on message "Pet" changed type from "enum" to "string".
proto/pet/v1/pet.proto:47:3:Field "1" with name "pet_type" on message "PutPetRequest" changed type from "enum" to "string".
proto/pet/v1/pet.proto:56:3:Field "1" with name "pet_id" on message "DeletePetRequest" changed option "json_name" from "petID" to "petId".
proto/pet/v1/pet.proto:56:10:Field "1" on message "DeletePetRequest" changed name from "petID" to "pet_id".

第二和第三个错误是您刚刚所做的更改 — PetType 同时用于 PetPutPetRequest 消息中。其他三个错误来自您之前应用的代码检查修复 — 将 petID 重命名为 pet_id 以及将 PetStore 重命名为 PetStoreService 也是 wire 级别和生成代码级别的破坏性变更。在实际项目中,您需要解决所有这些问题,但为了本快速入门,您可以暂时忽略它们。

还原更改

确定您的更改是破坏性变更后,请将其还原:

message Pet {
- string pet_type = 1;
+ PetType pet_type = 1;
  string pet_id = 2;
  string name = 3;
}

实现一个 API

在本节中,您将实现一个 PetStoreService 客户端和服务器,两者都可以在命令行上运行。

初始化 go.mod 文件

在编写 Go 代码之前,使用 go mod init 命令初始化 go.mod 文件:

go mod init github.com/bufbuild/buf-examples

模块路径与克隆的仓库匹配,以便生成的代码和服务器代码中的导入路径能够正确解析。在您自己的项目中,这将是您的实际模块路径(例如 github.com/your-org/your-repo)。与 buf.yaml 文件类似,go.mod 文件跟踪代码的 Go 依赖项。

实现服务器

通过创建 server/main.go 文件开始实现服务器:

$ mkdir server
$ touch server/main.go

将以下内容复制并粘贴到该文件中:

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    petv1 "github.com/bufbuild/buf-examples/gen/pet/v1"
    "github.com/bufbuild/buf-examples/gen/pet/v1/petv1connect"
    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"
)

const address = "localhost:8080"

func main() {
    mux := http.NewServeMux()
    path, handler := petv1connect.NewPetStoreServiceHandler(&petStoreServiceServer{})
    mux.Handle(path, handler)
    fmt.Println("... Listening on", address)
    http.ListenAndServe(
        address,
        // 使用 h2c 以便无需 TLS 即可提供 HTTP/2 服务。
        h2c.NewHandler(mux, &http2.Server{}),
    )
}

// petStoreServiceServer 实现了 PetStoreService API。
type petStoreServiceServer struct {
    petv1connect.UnimplementedPetStoreServiceHandler
}

// PutPet 将给定请求关联的宠物添加到宠物商店中。
func (s *petStoreServiceServer) PutPet(
    _ context.Context,
    req *petv1.PutPetRequest,
) (*petv1.PutPetResponse, error) {
    pet := &petv1.Pet{Name: req.GetName(), PetType: req.PetType}
    log.Printf("PutPet received a %v named %s", pet.GetPetType(), pet.GetName())
    return &petv1.PutPetResponse{Pet: pet}, nil
}

解析 Go 依赖项

现在您已经有了服务器代码,运行以下命令来解析构建代码所需的依赖项:

$ go mod tidy

调用 API

使用上面 server/main.go 的实现,运行服务器并使用 buf CLI 调用 PutPet 端点。

首先,运行服务器:

go run server/main.go
... Listening on localhost:8080

在另一个终端中,在工作区根目录下,使用 buf curl 调用 API 将宠物添加到商店:

buf curl \
  --schema . \
  --data '{"pet_type": "PET_TYPE_SNAKE", "name": "Ekans"}' \
  http://localhost:8080/pet.v1.PetStoreService/PutPet

{
  "pet": {
    "petType": "PET_TYPE_SNAKE",
    "name": "Ekans"
  }
}

--schema . 告诉 buf curl 使用本地工作区来解析请求和响应的 Protobuf 模式。如果没有它,buf curl 将需要服务器支持 gRPC server reflection

请注意,响应使用了小驼峰命名字段名(petType 而不是 pet_type)— 这是标准的 proto3 JSON 编码,它将 snake_case 的 Protobuf 字段名映射为 lowerCamelCase

回到服务器终端窗口,您会看到请求已被接收:

2025/06/13 10:06:25 PutPet received a PET_TYPE_SNAKE named Ekans

总结

这是一个基本的演练,您已经学习了 Buf CLI 的关键功能:

  1. 设置本地工作区。
  2. 生成代码以使用您选择的语言实现 API。
  3. 对文件进行代码检查以提高代码质量和一致性。
  4. 检测破坏性变更以避免破坏下游消费者。
  5. 使用集成生成代码的服务器应用程序访问您的 API。

要了解如何在大型组织中更有效地使用 Protobuf 模式,请接着完成 Buf Schema Registry 快速入门

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐