345 lines
11 KiB
Markdown
345 lines
11 KiB
Markdown
---
|
||
tags: [gRPC, Protobuf, protoc, Makefile, go generate, buf]
|
||
create time: 2026-05-11 16:40
|
||
---
|
||
|
||
# protoc 工具链与 Makefile
|
||
|
||
## 概述
|
||
|
||
Protobuf 编译器 protoc 是整个体系的核心工具。配合不同语言的 code generator plugin,它能从同一份 .proto 文件生成各语言的类型和 stub。掌握 toolchain 的正确用法,可以避免无数个 "mismatched version" 报错。
|
||
|
||
## 核心组件矩阵
|
||
|
||
| 工具 | 作用 | 安装方式 |
|
||
|------|------|---------|
|
||
| `protoc` | 编译引擎 | 下载 binary 包 |
|
||
| `protoc-gen-go` | 生成 Go struct | `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest` |
|
||
| `protoc-gen-go-grpc` | 生成 Go gRPC stub | `go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest` |
|
||
| `protoc-gen-js` | 生成 JS 代码 | npm 安装 |
|
||
| `protoc-gen-java` | 生成 Java 代码 | Maven/Bazel 自带 |
|
||
| `protoc-gen-python` | 生成 Python 代码 | pip 安装 |
|
||
|
||
> [!tip] 安装验证
|
||
> 安装后运行 `protoc --version` 确认版本,并检查 `which protoc-gen-go` 是否指向 GOPATH/bin。这是排查问题的第一步。
|
||
|
||
## 基本命令
|
||
|
||
最简单的单文件生成:
|
||
|
||
```bash
|
||
protoc --go_out=. --go-grpc_out=. api/user/v1/user.proto
|
||
```
|
||
|
||
批量生成整个 proto 目录:
|
||
|
||
```bash
|
||
protoc --go_out=. --go-grpc_out=. \
|
||
--go_opt=paths=source_relative \
|
||
--go-grpc_opt=paths=source_relative \
|
||
$(find . -name "*.proto")
|
||
```
|
||
|
||
这里的关键参数是 `--go_opt=paths=source_relative`——它是路径模式的选择,下一节详解。
|
||
|
||
> [!question] 为什么需要两个 --*_out?
|
||
> `--go_out` 生成消息结构体(pb.go),`--go-grpc_out` 生成 client/server stub(_grpc.go)。二者缺一不可,少了哪个都不会编译通过。
|
||
|
||
## 核心流程图解
|
||
|
||
理解数据流向是正确使用工具链的前提:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph Source["📁 Proto 源文件"]
|
||
P1["user.proto"]
|
||
P2["status.proto"]
|
||
end
|
||
|
||
subgraph Engine["⚙️ protoc 编译引擎"]
|
||
Parse["解析 .proto"]
|
||
Validate["验证 package / import"]
|
||
Dispatch["按 --*_out 分发"]
|
||
end
|
||
|
||
subgraph Plugins["🔌 Code Generator Plugins"]
|
||
Gopb["protoc-gen-go\n→ *.pb.go"]
|
||
Grpc["protoc-gen-go-grpc\n→ *_grpc.go"]
|
||
Other["protoc-gen-js / others"]
|
||
end
|
||
|
||
subgraph Output["📦 生成结果"]
|
||
U["user.pb.go + user_grpc.go"]
|
||
S["status.pb.go"]
|
||
end
|
||
|
||
P1 --> Parse
|
||
P2 --> Parse
|
||
Parse --> Validate
|
||
Validate --> Dispatch
|
||
Dispatch -->|--go_out| Gopb
|
||
Dispatch -->|--go-grpc_out| Grpc
|
||
Dispatch -->|其他插件| Other
|
||
Gopb --> U
|
||
Grpc --> U
|
||
Gopb --> S
|
||
```
|
||
|
||
> [!tip] 核心要点
|
||
> protoc 本身 **不生成任何语言代码**——它只是一个解析器和调度器。真正的代码生成由 `protoc-gen-*` 插件完成。这就是为什么缺少某个 plugin 时会直接报错 "program not found"。
|
||
|
||
## 路径模式对比(关键!)
|
||
|
||
这是初学者最常踩的坑:
|
||
|
||
```bash
|
||
# source_relative(v2 唯一官方推荐)
|
||
--go_opt=paths=source_relative
|
||
|
||
# (无此选项 = 旧版默认行为,已废弃)
|
||
# 会尝试根据 import 和 go_package 计算输出路径
|
||
```
|
||
|
||
`source_relative` 模式下,输出路径相对于 .proto 文件的 import path,符合 Go module 约定。比如:
|
||
|
||
```protobuf
|
||
// api/user/v1/user.proto
|
||
package user.v1;
|
||
option go_package = "github.com/mycompany/platform/api/user/v1;v1";
|
||
```
|
||
|
||
在 `source_relative` 下,生成的文件位于 `api/user/v1/user.pb.go`,与源文件同目录。旧版默认行为(不指定 `--go_opt=paths`)会根据 `go_package` 中的 module path 计算输出位置,经常导致路径混乱。**新版 protoc-gen-go(v2+)中该行为已移除,必须显式指定 `source_relative`。**
|
||
|
||
> [!warning] 新版 breaking change
|
||
> `paths=import_path_provided` 已在 protoc-gen-go v2 中移除。**必须显式指定** `--go_opt=paths=source_relative`,否则编译直接报错。升级前请确认所有 proto 生成命令都已添加此参数。
|
||
|
||
## go generate 集成
|
||
|
||
两种方式可以将 protoc 集成到开发流程中:
|
||
|
||
### 方式一:Makefile 集中管理
|
||
|
||
基础版——适合快速上手:
|
||
|
||
```makefile
|
||
PROTO_DIR = api
|
||
PROTOS := $(shell find $(PROTO_DIR) -name "*.proto")
|
||
|
||
.PHONY: proto
|
||
proto:
|
||
protoc \
|
||
--proto_path=$(PROTO_DIR) \
|
||
--go_out=$(PROTO_DIR) \
|
||
--go_opt=paths=source_relative \
|
||
--go-grpc_out=$(PROTO_DIR) \
|
||
--go-grpc_opt=paths=source_relative \
|
||
$(PROTOS)
|
||
```
|
||
|
||
进阶版——带增量构建和依赖追踪,生产项目推荐:
|
||
|
||
```makefile
|
||
PROTO_DIR = api
|
||
PROTOS := $(shell find $(PROTO_DIR) -name "*.proto")
|
||
|
||
# 每个 .proto 对应一个 stamp 文件,记录生成时间
|
||
STAMPS := $(patsubst $(PROTO_DIR)/%.proto,.gen/%.proto.done,$(PROTOS))
|
||
|
||
.gen/%.proto.done: $(PROTO_DIR)/%.proto
|
||
@mkdir -p .gen
|
||
protoc --proto_path=$(PROTO_DIR) \
|
||
--go_out=$(PROTO_DIR) --go_opt=paths=source_relative \
|
||
--go-grpc_out=$(PROTO_DIR) --go-grpc_opt=paths=source_relative \
|
||
$<
|
||
touch $@
|
||
|
||
.PHONY: proto clean
|
||
proto: $(STAMPS)
|
||
clean:
|
||
rm -rf .gen $(PROTO_DIR)/**/*.pb.go $(PROTO_DIR)/**/*_grpc.go
|
||
```
|
||
|
||
设计要点:
|
||
|
||
- **增量构建**:只重新编译有变化的 `.proto` 文件,大项目节省大量时间。
|
||
- **Stamp 文件**:利用 Make 的 timestamp 机制自动判断是否需要重建。
|
||
- **`$<`**:Make 自动变量(非 shell),代表当前规则的第一个依赖文件。
|
||
|
||
> [!tip] `--proto_path` 详解
|
||
> 这个参数指定 `.proto` 文件的搜索根目录。所有 `import "xxx.proto"` 语句中的路径都是相对于 `--proto_path` 计算的。
|
||
>
|
||
> ```bash
|
||
> # ✅ 正确:proto_path 指向 proto 文件所在目录
|
||
> protoc --proto_path=api api/user/v1/user.proto
|
||
> # user.proto 中 import "v1/status.proto" 可以正确解析
|
||
>
|
||
> # ❌ 错误:proto_path 指向了上级目录
|
||
> protoc --proto_path=. api/user/v1/user.proto
|
||
> # import 路径需要写成 "api/user/v1/status.proto",容易出错且不统一
|
||
> ```
|
||
|
||
### 方式二:go generate 内联
|
||
|
||
在 `.proto` 同级或 Go 包目录下放置 `//go:generate` 指令:
|
||
|
||
```go
|
||
//go:generate protoc --proto_path=../api --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative user/v1/user.proto
|
||
package user
|
||
```
|
||
|
||
然后运行 `go generate ./...` 触发指定包的代码生成。
|
||
|
||
> [!note] 两种方式的取舍
|
||
> - **Makefile** 适合多语言、大项目,统一入口、易 CI 化。
|
||
> - **go generate** 适合小项目或单体仓库,每个包自包含,不需要额外工具。
|
||
|
||
## 版本管理
|
||
|
||
正确的做法是将 runtime library 和 code generator 的版本对齐:
|
||
|
||
```toml
|
||
require (
|
||
google.golang.org/protobuf v1.36.6 // indirect
|
||
google.golang.org/grpc v1.74.2
|
||
)
|
||
```
|
||
|
||
关键点:
|
||
|
||
| 组件 | 版本对齐规则 |
|
||
|------|-------------|
|
||
| `protoc` 二进制 | 仅需与 protobuf **Library** 兼容,参考 [官方兼容性矩阵](https://github.com/protocolbuffers/protobuf/releases) |
|
||
| `protoc-gen-go` | minor 版本应与 `google.golang.org/protobuf` 一致 |
|
||
| `protoc-gen-go-grpc` | minor 版本应与 `google.golang.org/grpc` 一致 |
|
||
|
||
常用检查命令:
|
||
|
||
```bash
|
||
# 查看 protoc 编译器版本
|
||
protoc --version
|
||
# libprotoc 5.28.2
|
||
|
||
# 查看 Go runtime 版本
|
||
go list -m google.golang.org/protobuf
|
||
# google.golang.org/protobuf v1.36.6
|
||
|
||
# 查看 generator 版本
|
||
protoc-gen-go --version
|
||
# v1.36.6
|
||
```
|
||
|
||
> [!warning] 常见陷阱
|
||
> 如果你用 `go install` 安装了最新版的 protoc-gen-go,但 go.mod 里锁的是旧版 library,就可能遇到 "field number X is out of range" 之类的诡异错误。**始终让 generator 和 library 版本对应。**
|
||
>
|
||
> ```bash
|
||
> # 推荐的锁定方式——在 go.mod 中显式 require generator
|
||
> go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.6
|
||
> go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
|
||
> ```
|
||
|
||
## 多语言生成(Buf 推荐方案)
|
||
|
||
当项目需要同时产出 Go、Rust、JS 等多语言代码时,手动拼 protoc 脚本会变得异常复杂。推荐使用 Buf:
|
||
|
||
```yaml
|
||
# buf.gen.yaml
|
||
version: v2
|
||
plugins:
|
||
- remote: buf.build/community/neoeinstein-prost
|
||
out: gen/rs
|
||
- remote: buf.build/community/nipunn1313-grpc-javascript-client
|
||
out: gen/js
|
||
- local: protoc-gen-go
|
||
out: gen/go
|
||
```
|
||
|
||
配合 `buf.yaml` workspace 配置:
|
||
|
||
```yaml
|
||
# buf.yaml
|
||
version: v2
|
||
modules:
|
||
- path: api
|
||
```
|
||
|
||
一条命令搞定:`buf generate`。
|
||
|
||
> [!tip] 为什么推荐 Buf?
|
||
> - 内置 lint 和 breaking change 检测
|
||
> - 自动处理 plugin 版本管理和缓存
|
||
> - 声明式配置取代脆弱的 shell 脚本
|
||
> - 远程 plugin 无需本地安装
|
||
|
||
## 常见问题排查
|
||
|
||
| 症状 | 原因 | 解决 |
|
||
|------|------|------|
|
||
| "protoc-gen-go: program not found" | PATH 不包含 GOPATH/bin | `export PATH=$PATH:$(go env GOPATH)/bin` |
|
||
| "mismatched versions" | protoc-gen-go 与 library 版本不一致 | 对齐到相同的 minor 版本 |
|
||
| "import not found" | --proto_path 未指定或相对路径错误 | 确保 --proto_path 指向 .proto 根目录 |
|
||
| 生成文件为空 | go_package option 缺失或格式错误 | 检查 `option go_package = "module/path;pkg"` 格式 |
|
||
| panic: proto: field XXX has bad tag | .proto 文件版本混乱 | 清理所有 pb.go 后重新生成 |
|
||
|
||
### protoc vs Buf:工作流对比
|
||
|
||
> [!question] protoc 和 Buf 应该选哪个?
|
||
> 简单说:**小项目用原生 protoc,多语言或中大型项目用 Buf**。详细对比见下方流程图。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph Protoc["直接 protoc"]
|
||
A1["写 .proto"] --> B1["手写 shell / Makefile"]
|
||
B1 --> C1["拼 --*_out 参数"]
|
||
C1 --> D1["手动管理 plugin 版本"]
|
||
D1 --> E1["运行生成"]
|
||
end
|
||
|
||
subgraph Buf["Buf 封装"]
|
||
A2["写 .proto"] --> B2["写 buf.yaml + buf.gen.yaml"]
|
||
B2 --> C2["buf generate"]
|
||
C2 --> D2["自动下载/缓存 plugin"]
|
||
D2 --> E2["运行生成 + lint"]
|
||
end
|
||
|
||
style A1 fill:#FEF08A
|
||
style A2 fill:#DBEAFE
|
||
style E1 fill:#FEE2E2,color:#991B1B
|
||
style E2 fill:#D1FAE5,color:#065F46
|
||
```
|
||
|
||
| 维度 | 直接 protoc | Buf |
|
||
|------|------------|-----|
|
||
| **配置方式** | shell / Makefile 脚本 | YAML 声明式配置 |
|
||
| **Plugin 管理** | 手动 `go install`,易版本混乱 | 自动下载、缓存、锁定版本 |
|
||
| **Lint** | 无,需自建规则 | 内置 `buf lint`,可定制规则 |
|
||
| **Breaking Change** | 无 | `buf breaking` 自动检测 API 变更 |
|
||
| **学习成本** | 低,理解 protoc 即可 | 需额外了解 buf 概念(module/breaking) |
|
||
| **适合场景** | 单语言、小型 Go 服务 | 多语言、API 团队、跨服务协作 |
|
||
|
||
## 工具选型决策图
|
||
|
||
> [!question] 项目中 protoc / go generate / Buf 怎么选?
|
||
> 下面这张矩阵图展示了各工具在"复杂度"和"管理程度"两个维度上的定位:
|
||
|
||
```mermaid
|
||
quadrantChart
|
||
title 工具链选型矩阵
|
||
x-axis Low Complexity --> High Complexity
|
||
y-axis Native protoc --> Managed Workflow
|
||
quadrant-1 "多语言大项目"
|
||
quadrant-2 "小 Go 项目"
|
||
quadrant-3 "轻量脚本"
|
||
quadrant-4 "中型 Go 服务"
|
||
"Buf": [0.8, 0.9]
|
||
"go generate": [0.3, 0.7]
|
||
"Makefile": [0.5, 0.65]
|
||
"shell 脚本": [0.15, 0.2]
|
||
```
|
||
|
||
## 关联笔记
|
||
|
||
- [[hhs/gRPC/README.md]] — gRPC 知识库总览
|
||
- [[hhs/gRPC/1. Protobuf 基础篇/01-Protobuf 语法与消息定义.md]] — Protobuf 语法基础
|
||
- [[hhs/gRPC/2. gRPC 核心篇/06-Service 定义与代码生成.md]] — Service 定义与代码生成
|
||
- [[hhs/gRPC/6. 工程实践篇/18-模块拆分与 proto 规范.md]] — Proto 文件组织规范
|