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 文件组织规范
|