代码生成:protoc 与 buf,Go 工程目录约定

.proto 写得再规范,Go 代码也不会自己出现。这一步要做三件事:装好编译器与插件、把 .proto 的目录结构定下来、把生成命令固化成一条可重复执行的动作。手敲两条 protoc 命令撑一周还行,接口一多必然出错,所以工程上通常直接上 buf。

装工具:protoc 本体与两个 Go 插件

protoc 是编译器本体(C++ 写的命令行工具),Go 代码由插件生成,两者都要装:

# 1) 装 protoc 本体:下载官方预编译包解压后把 bin 加进 PATH
#    也可用 brew install protobuf / apt install -y protobuf-compiler
#    具体版本与包名以官方文档为准

# 2) 装两个 Go 插件:一个出 message,一个出 service
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# 3) 确认插件可用:protoc 靠 PATH 找可执行文件,名字是约定死的
go env GOPATH            # 插件落在 $(go env GOPATH)/bin,把该目录加进 PATH
protoc-gen-go --version
protoc-gen-go-grpc --version

命名约定是关键:看到 --go_out,protoc 就去找名为 protoc-gen-go 的可执行文件;看到 --go-grpc_out 就找 protoc-gen-go-grpc。找不到时的报错是 protoc-gen-go: program not found or is not executable。

直接用 protoc

protoc \
  --proto_path=proto \
  --go_out=gen --go_opt=paths=source_relative \
  --go-grpc_out=gen --go-grpc_opt=paths=source_relative \
  proto/account/v1/account.proto
  • --proto_path(可简写 -I)是 import 的解析根目录;import "google/protobuf/timestamp.proto" 这类标准库路径由 protoc 自带的 well-known types 提供,不用自己抄一份。
  • paths=source_relative 让输出路径跟着 .proto 相对 --proto_path 的路径走:proto/account/v1/account.proto 生成 gen/account/v1/account.pb.go。不加这个选项时,插件按 go_package 的导入路径铺目录,容易生成到意料之外的位置。
  • 每个 .proto 产出两个文件:account.pb.go(消息与序列化)和 account_grpc.pb.go(客户端与服务端骨架),两者都不要手改。

用 buf:把命令变成配置

buf 是一套 protobuf 工具链,自带 lint、破坏性变更检测与依赖管理,配置比一长串 protoc 参数耐维护得多。

buf.yaml 声明模块与检查规则:

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

buf.gen.yaml 声明插件与输出位置:

version: v2
plugins:
  - local: protoc-gen-go
    out: gen
    opt: paths=source_relative
  - local: protoc-gen-go-grpc
    out: gen
    opt: paths=source_relative

日常命令:

buf lint        # 按 STANDARD 规则检查命名、目录与注释
buf format -w   # 统一 .proto 排版
buf generate    # 按 buf.gen.yaml 批量生成,等价于跑一遍 protoc
buf breaking --against '.git#branch=main'   # 与 main 分支对比,检出破坏性改动

配置文件的 schema 会随 buf 大版本变化(v1 与 v2 差异明显),升级前以官方文档为准,别照抄旧文章的写法。

Go 工程目录约定

目录放什么约定
proto/<domain>/<version>/*.proto契约源文件,按领域加版本分目录版本进路径(v1、v2),不进文件名
gen/<domain>/<version>/*.pb.go生成物,整目录提交或整目录重建二选一顶部别手写代码
cmd/<service>/main.go每个服务的入口只做装配:读配置、建连接、起 server
internal/<service>/service.go生成接口的实现一个 proto service 一个文件
internal/<service>/store.go数据访问,与 RPC 层解耦便于单测替换

把版本写进目录的好处:im.account.v1 与 im.account.v2 可以长期共存,灰度期各服务各调各的,这正是第 15 章的兼容策略在工程结构上的落点。

把生成固化成一条命令

用 Makefile:

.PHONY: proto
proto:
	buf generate
	go build ./...

也可以挂到 Go 的 go:generate 上(前提是插件已安装并在 PATH 中):

package gen

//go:generate buf generate

CI 里加一行 buf lint && buf breaking --against '.git#branch=main' && buf generate && git diff --exit-code gen,就能挡住「改了 proto 忘了提交生成物」和「顺手引入不兼容改动」两类事故。

常见坑

坑现象改法
插件不在 PATHprotoc-gen-go: program not found把 $(go env GOPATH)/bin 加进 PATH 并重开终端
只跑 --go_out只有消息类型,没有 Client/Server--go-grpc_out 必须一起跑,事后补不出来
生成物的 import 路径很怪import 一个不存在的包,编译失败统一 go_package 与 paths=source_relative,导入根用模块名
手工修改生成物下次生成被覆盖,改动丢失要扩展行为就写包装类型,别改生成文件
生成物不提交、CI 也不生成拉下代码编译不过提交前统一 buf generate,或构建流水线里生成
protoc 与插件版本漂移生成结果 diff 满天飞在 CI 与本地固定同一版本,版本号写进脚本
把 .proto 放在实现代码目录里契约与实现耦合,复用困难契约集中收在 proto/ 下

小结:protoc 负责编译、插件负责出 Go 代码、buf 把两者收进配置文件;契约放 proto/<domain>/<version>/、生成物放 gen/、实现在 internal/,并让 buf generate 成为 CI 上可重复执行的一步。

笔记加载中…