diff --git a/hhs/gRPC/1. Protobuf 基础篇/03-字段编号与前向兼容.md b/hhs/gRPC/1. Protobuf 基础篇/03-字段编号与前向兼容.md index 13e5fbf..94edb53 100644 --- a/hhs/gRPC/1. Protobuf 基础篇/03-字段编号与前向兼容.md +++ b/hhs/gRPC/1. Protobuf 基础篇/03-字段编号与前向兼容.md @@ -174,8 +174,30 @@ reserved "legacy_id", "temp_field", 20 to 25, 30; > [!warning] Proto2 用户注意 > proto2 **没有** `reserved` 关键字!如果你在做 proto2 → proto3 迁移,原来依赖文档规范的 reserved 行为在 proto3 中可以真正落到代码里了——这是一大收益。 -> [!question] 思考题 -> 如果一个字段被删除了,但**没有**做 reserved,随后同事添加了 `string new_feature = 5;`,此时旧客户端读到 `new_feature` 的值时会发生什么?它会当成哪个字段的值? +> [!question]- 思考题 — 点击查看解析 +> +> **问题一**:假设你在线上跑着 `GetUser` API,客户端和服务端都稳定运行了两年。现在你需要添加 `avatar_url` 字段并删除 `phone` 字段。**你能在不重启任何服务、不升级任何客户端的前提下完成这件事吗?** +> +> **答案**:可以,但需要正确管理 field numbers。这就是本篇笔记要讲的事。 +> +> --- +> +> **问题二**:如果一个字段被删除了,但**没有**做 reserved,随后同事添加了 `string new_feature = 5;`,此时旧客户端读到 `new_feature` 的值时会发生什么?它会当成哪个字段的值? +> +> **答案**:Protobuf 的二进制 wire encoding 中**不包含字段名**,只包含: +> +> ``` +> field_number + wire_type + value_bytes +> ``` +> +> 解析器完全按照 **field number 为键**来解码。它不知道 `5` 这个编号以前代表什么、现在又被分配给了谁——它只认数字。所以旧客户端会读到的字节流,将 `new_feature` 的值当成之前被删除的旧字段(原编号 5 对应的 `mobile`)的值。 +> +> | 角色 | 服务端 proto (v2: `new_feature = 5`) | 旧客户端 proto (v1: `mobile = 5`, 无 reserved) | +> |------|------|------| +> | 发出去的内容 | `field_number=5, type=string, value="dark_mode"` | 期望读到手机号 | +> | 旧客户端解读 | ❌ `"dark_mode"` → 当手机号入库 | 数据语义错位 | +> +> 这就是为什么笔记里那条铁律:**field number 一旦使用过,就永远不能再复用**。正确的做法是删除时做 `reserved`,让编译器在后续有人复用该编号时直接报错。 ### 删除字段的正确姿势