跳转到正文
zeno's blog
返回

Go 工具链:Buf 如何替代 protoc 工作流

Table of contents

Open Table of contents

TL;DR

Buf 用一个 CLI 统一了 protobuf 的构建、lint、breaking change 检测、代码生成、格式化和依赖管理。它替代的不是 protobuf 本身,而是 protoc + 手动管理插件 + Makefile/shell 脚本的传统工作流。核心价值:buf.yaml 定义模块和规则,buf.gen.yaml 定义代码生成,buf generate 一个命令完成所有事。


Buf 解决的问题:protoc 的痛点

传统 protoc 工作流的典型状态:

# Makefile 里的 protoc 调用 — 项目越大越失控
generate:
	protoc \
		-I ./proto \
		-I ./third_party/googleapis \           # 手动下载的依赖
		-I $(GOPATH)/src/github.com/grpc-ecosystem/grpc-gateway \  # 路径可能不对
		--go_out=./gen --go_opt=paths=source_relative \
		--go-grpc_out=./gen --go-grpc_opt=paths=source_relative \
		--grpc-gateway_out=./gen --grpc-gateway_opt=paths=source_relative \
		--openapiv2_out=./openapi \
		./proto/**/*.proto
protoc 的痛点Buf 怎么解决
插件要手动安装、版本不一致remote plugin 从 BSR 拉取,版本锁定
依赖(googleapis 等)要手动下载deps 字段声明,buf dep update 拉取
import 路径容易出错模块系统自动解析
没有 lint 规则内置 lint,检查命名规范、包结构等
breaking change 靠人眼审buf breaking 自动检测
Makefile 越写越长buf generate 一个命令
CI 里要装 protoc + 所有插件CI 里只装 buf

安装

# macOS
brew install bufbuild/buf/buf

# Linux
curl -sSL https://github.com/bufbuild/buf/releases/latest/download/buf-Linux-x86_64 \
  -o /usr/local/bin/buf && chmod +x /usr/local/bin/buf

# 验证
buf --version

核心配置文件

Buf 的一切行为由两个 YAML 文件控制。

buf.yaml — 模块定义 + lint 规则 + breaking 策略

version: v2
modules:
  - path: proto # proto 文件根目录
    name: buf.build/yourorg/im # BSR 上的模块名(可选,发布时需要)
deps:
  - buf.build/googleapis/googleapis # google.api.http 注解
  - buf.build/bufbuild/protovalidate # 验证规则
lint:
  use:
    - STANDARD # 标准 lint 规则集
breaking:
  use:
    - FILE # 最严格的 breaking change 检测
# 拉取依赖(类似 go mod tidy)
buf dep update

buf.gen.yaml — 代码生成配置

version: v2
clean: true # 生成前清空 output 目录
managed:
  enabled: true # 自动管理 go_package 等 option
  override:
    - file_option: go_package_prefix
      value: github.com/yourorg/im-server/gen
plugins:
  # Go message/enum 类型
  - remote: buf.build/protocolbuffers/go
    out: gen/go
    opt: paths=source_relative

  # Go gRPC server/client
  - remote: buf.build/grpc/go
    out: gen/go
    opt: paths=source_relative

  # grpc-gateway HTTP 反向代理
  - remote: buf.build/grpc-ecosystem/gateway
    out: gen/go
    opt: paths=source_relative

  # grpc-gateway OpenAPI spec
  - remote: buf.build/grpc-ecosystem/openapiv2
    out: openapi

  # TypeScript(前端 proto 类型)
  - remote: buf.build/bufbuild/es
    out: gen/ts
    opt: target=ts

inputs:
  - directory: proto
# 一个命令生成所有代码
buf generate

日常工作流

代码生成

# 改了 proto 文件后,一个命令全搞定
buf generate

# 生成的文件结构
gen/
├── go/im/user/v1/
   ├── user.pb.go           # message 类型
   ├── user_grpc.pb.go      # gRPC server/client interface
   └── user.pb.gw.go        # grpc-gateway HTTP 映射
├── go/im/message/v1/
   └── ...
└── ts/im/user/v1/
    └── user_pb.ts            # TypeScript 类型

Lint — 强制命名规范

buf lint

# 典型输出:
# proto/user.proto:15:3: Field name "userName" should be lower_snake_case.
# proto/user.proto:1:1: Package name "User" should be lower_snake_case.

STANDARD 规则集包含的核心检查:

规则检查内容
FIELD_LOWER_SNAKE_CASE字段名必须 snake_case
SERVICE_SUFFIXService 名必须以 Service 结尾
RPC_REQUEST_RESPONSE_UNIQUE每个 RPC 的 Request/Response 不能复用
PACKAGE_DIRECTORY_MATCH包名和目录结构一致
ENUM_ZERO_VALUE_SUFFIXenum 零值必须以 _UNSPECIFIED 结尾
COMMENT_SERVICE / COMMENT_RPCservice 和 rpc 必须有注释

Breaking Change 检测

# 对比当前代码和 main 分支,检测 breaking change
buf breaking --against '.git#branch=main'

# 典型输出:
# proto/user.proto:32:3: Field "1" on message "RegisterRequest" changed type from "string" to "int64".
# proto/user.proto:1:1: Previously present service "UserService" was deleted.

检测级别:

级别检测内容适用场景
FILE字段类型/编号、service/rpc 删除、文件删除等所有变更推荐默认
PACKAGE同上但允许跨文件移动定义重组 proto 文件结构时
WIRE_JSON只检查影响序列化兼容性的变更第三方/公开 API
WIRE只检查二进制序列化兼容性最宽松

格式化

# 格式化所有 proto 文件(类似 gofmt)
buf format -w

Remote Plugin vs Local Plugin

# Remote plugin — 从 BSR 拉取,不需要本地安装
plugins:
  - remote: buf.build/protocolbuffers/go
    out: gen/go
    opt: paths=source_relative

# Local plugin — 使用本地安装的 protoc-gen-xxx
plugins:
  - local: protoc-gen-go
    out: gen/go
    opt: paths=source_relative
方式优点缺点
remote不需要安装插件,版本锁定,CI 友好首次拉取需要网络
local离线可用,可用自定义插件要手动安装和管理版本

推荐用 remote,除非有离线需求或用了不在 BSR 上的自定义插件。

Managed Mode — 自动管理 option

Proto 文件里的 option go_package 是重复的样板:

// ❌ 每个 proto 文件都要写,改包路径要改所有文件
option go_package = "github.com/yourorg/im-server/gen/im/user/v1;userv1";

Managed mode 自动生成这些 option,proto 文件里不用写:

# buf.gen.yaml
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: github.com/yourorg/im-server/gen
// ✅ proto 文件干净了
syntax = "proto3";
package im.user.v1;
// 不需要 option go_package,buf 自动算出来

依赖管理

# buf.yaml
deps:
  - buf.build/googleapis/googleapis # google.api.http
  - buf.build/bufbuild/protovalidate # buf.validate
  - buf.build/envoyproxy/protoc-gen-validate # pgv(如果还在用)
# 拉取/更新依赖
buf dep update
# 生成 buf.lock(类似 go.sum,锁定依赖版本)

依赖自动解析 import:

// 不需要手动下载 googleapis 到 third_party/
// buf 自动从 BSR 解析
import "google/api/annotations.proto";
import "buf/validate/validate.proto";

CI/CD 集成

# GitHub Actions
- name: Install Buf
  uses: bufbuild/buf-action@v1

- name: Lint
  run: buf lint

- name: Breaking change detection
  run: buf breaking --against 'https://github.com/yourorg/im-server.git#branch=main'

- name: Generate
  run: buf generate

- name: Check generated code is committed
  run: |
    git diff --exit-code gen/ || (echo "Generated code not committed" && exit 1)

关键:buf breaking 放在 CI 里,PR 改了 proto 如果有 breaking change 会自动拦截。 不靠人眼审,靠工具保证。

IM 项目的完整配置示例

目录结构

im-server/
├── proto/                        # proto 源文件
│   └── im/
│       ├── user/v1/
│       │   └── user.proto
│       ├── message/v1/
│       │   └── message.proto
│       └── ws/v1/
│           └── ws.proto
├── gen/                          # 生成代码(提交到 git)
│   ├── go/im/...
│   └── ts/im/...
├── openapi/                      # 生成的 OpenAPI spec
├── buf.yaml
├── buf.gen.yaml
└── buf.lock                      # 自动生成,锁定依赖版本

buf.yaml

version: v2
modules:
  - path: proto
deps:
  - buf.build/googleapis/googleapis
  - buf.build/bufbuild/protovalidate
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

buf.gen.yaml

version: v2
clean: true
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: github.com/yourorg/im-server/gen
plugins:
  - remote: buf.build/protocolbuffers/go
    out: gen/go
    opt: paths=source_relative
  - remote: buf.build/grpc/go
    out: gen/go
    opt: paths=source_relative
  - remote: buf.build/grpc-ecosystem/gateway
    out: gen/go
    opt: paths=source_relative
  - remote: buf.build/grpc-ecosystem/openapiv2
    out: openapi
  - remote: buf.build/bufbuild/es
    out: gen/ts
    opt: target=ts
inputs:
  - directory: proto

日常命令

buf dep update    # 更新依赖
buf lint          # 检查 proto 规范
buf format -w     # 格式化
buf generate      # 生成所有代码
buf breaking --against '.git#branch=main'  # PR 前检查 breaking change

Buf vs protoc 总结

维度protocBuf
插件管理手动安装 protoc-gen-xxxremote plugin 自动拉取
依赖管理手动下载到 third_party/deps 声明 + buf dep update
Lint内置 STANDARD/MINIMAL 规则集
Breaking changebuf breaking 自动检测
格式化无(有第三方 clang-format)buf format 内置
配置方式Makefile + shell 脚本buf.yaml + buf.gen.yaml
CI 友好度要装 protoc + 所有插件只装 buf
学习曲线低(但维护成本高)中(但一次配置长期受益)

Pitfalls

  1. clean: true 会删除 output 目录下的所有文件。如果你在 gen/ 目录下手写了代码(不应该),会被清掉。生成目录里只放生成代码
  2. remote plugin 首次运行需要网络。中国网络环境下可能超时,必要时换成 local plugin 并在 CI 里缓存插件二进制
  3. buf breaking 对比的是 proto 文件,不是生成代码。改了 proto 但忘了 buf generate,breaking 检测通过但生成代码过时。CI 里应该加 git diff --exit-code gen/ 检查
  4. managed mode 的 go_package_prefix 和 proto 里手写的 option go_package 冲突。用了 managed mode 就不要在 proto 里写 option go_package,否则行为不可预测
  5. buf lintSTANDARD 规则集可能比你预期的严格。比如要求每个 service 和 rpc 都有注释、enum 零值必须叫 XXX_UNSPECIFIED。初期不想全部遵守可以用 except 排除特定规则,但长期建议全部满足

分享这篇文章:

上一篇
Go 工具链:Protobuf 的字段编号、Varint 与二进制序列化
下一篇
现代 C++(五):内存模型、atomic、thread 与 future