This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hzh/MS/06-gRPC/02-Proto设计.md
T

196 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [grpc, protobuf, schema-versioning, proto3]
create time: 2026-05-07 16:00
---
# Proto 设计规范
## 概述
本文覆盖 **Proto3 进阶特性**和**版本管理策略**。如果说架构篇是"看懂 gRPC 长什么样",这篇就是教你"如何写出经得起演进的 Proto 文件"——这是工程中最容易踩坑、却最容易被忽视的部分。
> [!question] 思考
>
> 你的 Service A 调用 Service B 的 `GetUser`,Proto 里定义了 20 个字段。半年后你想加第 21 个字段,但旧版客户端没有编译更新。会发生什么?
好消息是:Protocol Buffers 的二进制编码规则天然保证了向前兼容。坏消息是:**只有遵守规则的改动才是兼容的**,违反规则的静默破坏会让你排查整整一天。
## Oneof —— 互斥字段
当某个消息体可能有多种不同类型的值,但同一时刻只出现一种时,使用 `oneof`:
```go
// proto/order/v1/order.proto
message Payment {
string order_id = 1;
oneof method {
AlipayPayment alipay = 2;
WechatPayment wechat = 3;
CreditCard card = 4;
}
}
message AlipayPayment {
string token = 1;
}
message WechatPayment {
string openid = 1;
}
message CreditCard {
string last_four = 1;
string exp_date = 2;
}
```
> [!note] Proto3 oneof 的限制
>
> - 不能加 `repeated` 修饰符
> - 空 oneof 值为 `NONE(0)`,反序列化时各字段都为默认值
> - **向后兼容策略**:新增 oneof 变体是安全的;删除变体会破坏旧客户端
## Well-Known Types —— 内置类型库
Proto3 内置了一些常用类型,直接引用即可,不用自己定义:
```proto
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";
message Event {
string name = 1;
google.protobuf.Timestamp created_at = 2; // 替代手动时间字符串
google.protobuf.Empty body = 3; // 空消息占位
google.protobuf.StringValue desc = 4; // 可选字符串(区分 unset 和空串)
}
```
| 类型 | 用途 |
|------|------|
| `Timestamp` | RFC 3339 纳秒级时间戳 |
| `Duration` | 带单位的时长 |
| `Empty` | 无参数 / 无返回值的 RPC 占位 |
| `StringValue / Int32Value / BoolValue` | 包装基本类型,表示"可选"语义 |
| `Struct / Value / ListValue` | 类 JSON 的动态结构 |
| `Any` | 泛型消息容器(需搭配 type URL) |
> [!tip] StringValue 的妙用
>
> Proto3 默认把未设置的字段归入"默认值",导致无法区分"用户传了空字符串"和"用户没传这个字段"。用 `StringValue` 包装后,Proto3 可以精确表达 Optional 语义。
## Map 字段
```proto
message Config {
map<string, string> labels = 1;
map<int32, float> price_cache = 2;
}
```
- **有序遍历**:Proto3 的 map 按 key 排序遍历
- **兼容性**:删除整个 map 字段可接受,删除其中某个 key-value 对**不安全**(会被当作未知字段忽略)
## 版本管理与向前兼容规则
这是工程中最容易踩坑的部分:
```mermaid
graph LR
Rule["向前兼容三大铁律"] --> R1["新增字段 → 旧客户端忽略"]
Rule --> R2["删除字段 → 新客户端忽略"]
Rule --> R3["字段编号永不重用"]
R1 -.-> F1["新字段标记 optional"]
R2 -.-> D1["标记 deprecated 而非删除"]
R3 -.-> N1["预留号段:1~19 用于 Google, 20~10000 自定"]
```
### 兼容性决策表
| 操作 | 是否安全 | 说明 |
|------|---------|------|
| 新增字段(用更大编号) | ✅ 安全 | 旧版客户端忽略未知编号字段 |
| 删除字段 | ⚠️ 部分安全 | 建议标记 `deprecated`,保留编号 |
| 修改字段类型 | ❌ 危险 | 可能导致二进制解析失败 |
| 重编字段编号 | ❌ 致命 | 读写两边理解错位 |
| 修改字段名 | ✅ 安全 | 名称不影响二进制编码 |
| 修改 enum 值 | ⚠️ 部分安全 | 新增安全,删除需用 reserved |
### 常见错误示范
```proto
// ❌ 错误做法:删掉 field 3 后把 field 4 改成 = 3
message BadExample {
string field1 = 1;
string field2 = 2;
// string removed_field = 3; ← 直接注释掉了
string new_field = 3; // ← 重编了!危险!
}
// ✅ 正确做法:保留编号并标记 reserved
message GoodExample {
string field1 = 1;
string field2 = 2;
reserved 3; // 锁定已删除字段的编号
string new_field = 4; // 用新编号
}
```
### reserved 的完整写法
```proto
message OldMessage {
reserved 2, 15; // 单个编号
reserved 9, 10, 14; // 连续范围可用语法 reserved 9 to 14;
reserved "name", "email"; // 已废弃的字段名
}
```
> [!warning] reserved 不是可选项
>
> 当你要删除一个字段或其编号时,必须加上 `reserved` 声明。否则未来有人新增字段时重新使用了这个编号,就会造成静默的数据损坏——旧客户端读到新字段数据当成旧字段解析。
## Proto 组织规范
推荐的目录结构:
```
proto/
├── buf.gen.yaml # Buf 代码生成配置
├── api/
│ └── v1/
│ ├── order/
│ │ ├── order.proto # Service + Message 定义
│ │ └── error.proto # 统一错误码定义
│ ├── user/
│ │ └── user.proto
│ └── common/
│ └── page.proto # 分页等通用定义
└── gen/go/github.com/example/api/ # 自动生成代码
```
### 命名约定
| 元素 | 命名风格 | 示例 |
|------|---------|------|
| package | 小写,点分隔,含版本前缀 | `order.v1` |
| service | PascalCase + Service 后缀 | `OrderService` |
| rpc method | PascalCase | `CreateOrder`, `GetUserInfo` |
| message | PascalCase | `CreateOrderRequest` |
| enum | PascalCase + Status/Type 后缀 | `OrderStatus`, `RoleType` |
| field | snake_case | `user_id`, `created_at` |
> [!tip] 推荐的工具链
>
> 建议使用 **[Buf](https://buf.build)** 替代 protoc 直调。Buf 提供统一的依赖管理、linting 和 CI 集成,且屏蔽了 protoc 在不同语言间的命令差异。
## 关联笔记
- [[01-协议与架构]] — Proto 文件最终服务于协议栈中的 Generated Stub 层
- [[07-最佳实践]] — Buf 代码生成流程和生产配置
- [[02-服务治理/服务间通信]] — 不同序列化方案的性能对比