vault backup: 2026-05-12 15:26:17

This commit is contained in:
hhs
2026-05-12 15:26:17 +08:00
parent 0381430fdc
commit 0241faca23
4 changed files with 494 additions and 174 deletions
@@ -18,24 +18,23 @@ Protobuf 的类型系统看起来简单,但有很多容易被忽略的细节
```mermaid
graph TD
A["Protobuf 类型系统"] --> B["Scalar Types\n标量类型"]
A --> C["Composite Types\n复合类型"]
A --> D["Well-Known Types\n内置类型"]
Root["Protobuf\n类型系统"] --> Scalar["Scalar Types\n标量类型"]
Root --> Composite["Composite Types\n复合类型"]
Root --> WKT["Well-Known Types\n内置类型"]
B --> B1["整数系: int32 / int64 / uint32 / uint64 / sint32 / sint64"]
B --> B2["浮点系: float / double"]
B --> B3["其他: bool / string / bytes"]
Scalar --> SInt["整数系\nint32 / int64 / sint32 / ..."]
Scalar --> SFloat["浮点系\nfloat / double"]
Scalar --> SOther["其他\nbool / string / bytes"]
C --> C1["repeated\n(动态列表)"]
C --> C2["map<string, T>\n(键值对)"]
C --> C3["message\n(自定义结构)"]
C --> C4["oneof\n(互斥字段)"]
Composite --> Rep["repeated\n动态列表"]
Composite --> MapType["map\n键值对"]
Composite --> Msg["message\n自定义结构"]
Composite --> OneofT["oneof\n互斥字段"]
D --> D1["Timestamp\n(time.Time)"]
D --> D2["Duration\n(time.Duration)"]
D --> D3["StringValue\n(*string 指针)"]
D --> D4["Any / Value / Struct\n(通用 JSON)"]
D --> D5["FieldMask\n(partial update)"]
WKT --> WTime["时间类\nTimestamp / Duration"]
WKT --> WWrap["包装类\nStringValue / Int32Value / ..."]
WKT --> WGen["泛型类\nAny / Value / Struct"]
WKT --> WUtil["工具类\nFieldMask / Empty"]
```
### proto2 vs proto3 关键差异
@@ -105,17 +104,22 @@ message Offset {
> [!tip] float vs double 取舍
> HTTP/2 + TLS 已经压缩了网络传输,**节省几个字节对延迟的影响微乎其微**。优先选择 `float32`,除非你的业务需要 IEEE 754 双精度精度(如金融计算)。
> [!note] string vs bytes:不只是编码区别
> - `string` **必须是合法 UTF-8**,不合法的字节序列在解析时会报错。适合人类可读的文本内容。
> - `bytes` 不做编码校验,可以存任意二进制数据(图片、加密密文等),且在 Go/Java 中生成的是可变长度数组,追加元素更灵活。
> - 如果不确定对方语言的 UTF-8 实现是否严格,用 `bytes` 更安全——接收端自行解码。
### Varint 编码与 zigzag 的关系
很多人分不清 varint 和 zigzag,这里简单拆解:
很多人分不清 varint 和 zigzag,这里用一张图理清它们的关系:
```mermaid
graph LR
A["原始整数"] --> B{"是否为负数?"}
B -- 否 --> C["varint: 每 7 bits 一组, MSB 标记 continuation"]
B -- 是 --> D["zigzag: n → (n << 1) ^ (n >> 31)"]
D --> C
C --> E["变长字节序列: 小数字仅 1 byte"]
N["原始整数"] --> S{"是否负数?"}
S -- 否 --> V["varint 直接编码"]
S -- 是 --> Z["zigzag 变换\nn → (n << 1) ^ (n >> 31)"]
Z --> V
V --> E["变长字节序列\n小数字仅 1 byte"]
```
- **varint**:只处理非负数,数字越小占的字节越少。`1` 占 1 byte,`2^31` 占 5 bytes。
@@ -137,24 +141,13 @@ message TagList {
考虑一组 `repeated int32` 字段 `[1, 2, 3]`,两种编码方式的 wire format 对比:
```mermaid
block
column "Unpacked (legacy)"
B1["tag(1B)"] B2["val 1(1B)"] B3["tag(1B)"] B4["val 2(1B)"] B5["tag(1B)"] B6["val 3(1B)"]
style B1 fill:#f9d
style B3 fill:#f9d
style B5 fill:#f9d
note1["重复写 tag\n共 6 bytes"]
column "Packed (proto3 默认)"
C1["tag(1B)"] C2["len(1B)"] C3["val 1(1B)"] C4["val 2(1B)"] C5["val 3(1B)"]
style C1 fill:#9df
style C2 fill:#9df
style C3 fill:#dfd
style C4 fill:#dfd
style C5 fill:#dfd
note2["只写一次 tag\n共 5 bytes"]
```
| 部分 | Unpacked(proto2 需显式声明) | Packed(proto3 默认) |
|------|-------------------------------|------------------------|
| tag | 每个元素前都写一次 | 只在开头写一次 |
| length | 无,每个 value 独立跟随 tag | 在 tag 后附加总长度字节 |
| value | `tag + val` × N | `tag + len + val₁ + val₂ + ...` |
| `[1, 2, 3]` 示意 | `tag·1 ·val₁· tag·2 ·val₂· tag·3 ·val₃·` | `tag·len·val₁·val₂·val₃` |
| 总字节数 | 6B | 5B |
随着元素数量增长,差距越来越明显:
@@ -231,14 +224,30 @@ Protobuf 提供了所有标量类型的 wrapper,Go 中一一对应:
Protobuf 内置了一组通用的消息类型,称为 Well-Known Types(WKT),全部定义在 `google/protobuf/` 下。它们在不同语言中有各自的 native 映射,是实现跨语言兼容的关键。
核心 WWT 分类如下:
核心 WKT 分类如下:
```mermaid
graph LR
A["Well-Known Types"] --> B["日期/时间\nTimestamp / Duration"]
A --> C["可选包装\nWrapper Types × 7"]
A --> D["泛型/动态\nAny / Value / Struct"]
A --> E["实用工具\nFieldMask / Empty / ..."]
subgraph Time["时间类"]
T1["Timestamp"]
T2["Duration"]
end
subgraph Wrap["包装类"]
W1["StringValue"]
W2["Int32Value / BoolValue / ..."]
end
subgraph Gen["泛型类"]
G1["Any"]
G2["Value / Struct"]
end
subgraph Util["工具类"]
U1["FieldMask"]
U2["Empty"]
end
RootW["Well-Known Types"] --> Time
RootW --> Wrap
RootW --> Gen
RootW --> Util
```
### 时间相关:Timestamp & Duration
@@ -290,31 +299,102 @@ proto.ApplyFieldMask(&updatedUser, req.GetUser())
JSON 传递时也很简洁:`{ "updateMask": "display_name,email", "user": { "display_name": "新名字" } }`。
### Any:泛型消息容器
> [!tip] FieldMask 更深入的用法(嵌套路径、服务端校验等)参见 [[hhs/gRPC/1. Protobuf 基础篇/04-FieldMask 实战/FieldMask 实战]]。
`Any` 允许你在不知道具体消息类型的情况下传递消息,常用于事件总线或插件架构:
### Any:Protobuf 的"万能盒子"
想象你有一个快递盒(`Any`),里面可以装任何东西——只要这东西是 protobuf 消息就行。好处是你不需要提前知道盒子里是什么,只需要在拆盒子前查一张**"标签→物品类型"**对照表就行。
```protobuf
import "google/protobuf/any.proto";
message Event {
google.protobuf.Any payload = 1; // 任意 protobuf message
string event_id = 1;
google.protobuf.Any payload = 2; // 这个盒子可以装任何消息
}
```
反序列化时需要注册 type registry:
#### 盒子里到底存了什么?
每个 `Any` 在底层只存了两个东西:
| 字段 | 类型 | 作用 |
|------|------|------|
| `type_url` | 字符串 | 类似文件扩展名,告诉接收方"这玩意儿是什么类型" |
| `value` | 字节序列 | 具体消息的二进制数据 |
举个例子,如果 `payload` 里装的是 `OrderCreated` 消息,它长这样:
- `type_url = "type.googleapis.com/mypb.OrderCreated"` — **这是什么**
- `value = [序列化后的二进制字节...]` — **具体内容**
#### 完整使用流程(三步走)
**第 1 步:装箱(发送端)** — 把具体的消息塞进 `Any`
```go
// 注册已知类型
ptypes.RegisterAnyType(reflect.TypeFor[OrderCreated]())
// 假设你已经有了 OrderCreated 对象
order := &OrderCreated{OrderId: "123", Amount: 9900}
// 从 Any 中提取具体类型
event := &Event{}
payload, _ := ptypes.UnmarshalAny(event.Payload)
// 用 proto.MarshalAny 自动打包成 Any(内部做了 marshal + 设置 type_url)
anyPayload, _ := proto.MarshalAny(order)
event := &Event{
EventId: "evt-001",
Payload: anyPayload, // ✅ 装上盒了
}
```
> [!danger] 谨慎使用 Any
> `Any` 绕过了静态类型检查,滥用会导致调试困难。只在**真正的扩展点**(如插件系统、事件溯源)使用,不要用它来替代正常的消息设计。
**第 2 步:注册类型映射(反序列化端,只需一次)** — 告诉程序 `"type_url" → "什么类型"`
```go
// 在程序启动时全局注册一次即可,之后所有地方都能用
proto.RegisterName(
reflect.TypeFor[OrderCreated](),
"type.googleapis.com/mypb.OrderCreated",
)
```
这一步就像在邮局备案:"如果快递单上写的是 `type.googleapis.com/mypb.OrderCreated`,那里面就是 `OrderCreated` 这个类型的包裹。"不注册的话,收到包裹后程序不知道该怎么拆开。
**第 3 步:拆箱(接收端)** — 从 `Any` 中取出原始消息,有两种方式:
```go
event := ... // 收到 Event,其中 event.Payload 是 Any 类型
// 写法 A:你知道里面是什么类型(推荐 ✅)
var msg OrderCreated
if err := event.Payload.UnmarshalTo(&msg); err != nil {
log.Fatal(err)
}
// 此时 msg 已经是强类型的 OrderCreated,可以直接用 msg.OrderId
// 写法 B:你不知道里面是什么类型(适合通用框架)
payload, err := proto.UnmarshalAny(event.Payload)
if err != nil {
log.Fatal(err)
}
// payload 是 proto.Message 接口,需要类型断言
switch m := payload.(type) {
case *OrderCreated:
fmt.Println("订单:", m.OrderId)
case *UserRegistered:
fmt.Println("新用户:", m.Username)
}
```
> [!tip] 两种拆箱方式怎么选?
> - **90% 的场景用 UnmarshalTo**:你已经知道负载类型,这种方式编译期就能检查类型匹配,不会漏掉 case。
> - 只有在写事件总线、插件系统等真正"不知道负载类型"的场景才用 UnmarshalAny + switch。
#### 实际应用场景
- **事件溯源 / CQRS**:一个 `EventStream` 可以按顺序存储不同类型的领域事件(订单创建、支付成功、物流发货……)。
- **gRPC 插件架构**:主服务定义一个接收 `Any` 的方法,插件自己注册类型映射,互不依赖对方 proto 文件。
- **跨服务消息传递**:上游服务只管往 `Any` 里放东西,下游服务按需拆包,解耦两个服务之间的类型依赖。
> [!warning] Any 不是银弹
> 它绕过了静态类型检查,滥用会让调用链变得难以追踪。记住一条原则:**能在 proto 设计阶段明确的类型关系,就不要用 Any 模糊处理**。只在真正的"扩展点"使用。
## Map 类型细节
@@ -400,6 +480,14 @@ default:
> - **协议切换**:同一个连接支持多种子协议
> - **互斥配置**:比如渲染模式只能选一种(WebGL / Canvas / SVG)
> [!warning] Oneof 的副作用:设置一个字段会清除其他字段
> Oneof 字段是互斥的——给 `alipay_token` 赋值时,之前设好的 `wechat_pay_nonce` 会被自动清空。这在链式调用中容易造成隐蔽 bug:
> ```go
> req.WechatPayNonce = "abc" // 设为微信支付
> req.AlipayToken = "xyz" // 微信字段被静默清除!现在只走了支付宝
> ```
> 建议封装 Builder 模式来避免这种陷阱。
## 最佳实践总结
- **优先使用 `int32`**,除非确定数据范围超过 ±21 亿才用 `int64`。
@@ -407,12 +495,15 @@ default:
- **需要表达"可选"时优先考虑 wrapper types**,比 oneof 更简洁,比裸 scalar 更能区分零值和缺失。
- **timestamp 统一用 RFC3339 string**,跨语言互通性最好。
- **sint32/sint64** 仅在小范围内有正负波动的场景(如 offset、delta)中使用。
- **oneof 用在"多选一"的互斥场景**,而不是用来模拟 optional。
- **oneof 用在"多选一"的互斥场景**,而不是用来模拟 optional;注意设置一个字段会清空其他字段。
- **FieldMask 是实现 RESTful PATCH 语义的神器**,别自己解析 JSON 路径了。
- **慎用 Any**,只在真正的扩展点使用,避免绕过类型安全。
- **字符串内容选 `string`,二进制选 `bytes`**——后者不校验 UTF-8,在异构语言环境中更安全。
- **不要在 proto 中定义嵌套 map**:不支持 `map<string, map<...>>`,需要用 message + repeated 替代。
## 关联笔记
- [[hhs/gRPC/1. Protobuf 基础篇/01-Protobuf 语法与消息定义]] — Protobuf 语法入门,建议先读本篇再来看本文
- [[hhs/gRPC/1. Protobuf 基础篇/03-字段编号与前向兼容]] — 字段编号管理、向前向后兼容规则
- [[hhs/gRPC/1. Protobuf 基础篇/04-Oneof 与包装类型]] — Oneof 深度使用 + Wrapper Type 实战模式
- [[hhs/gRPC/1. Protobuf 基础篇/01-Protobuf 语法与消息定义]] — Protobuf 语法入门,包含 message/enum 等基础结构,建议先读本篇再来看本文
- [[hhs/gRPC/1. Protobuf 基础篇/03-字段编号与前向兼容]] — 字段编号管理、向前向后兼容规则,与本篇的零值语义紧密相关
- [[hhs/gRPC/1. Protobuf 基础篇/04-FieldMask 实战/FieldMask 实战]] — FieldMask 深入:嵌套路径、服务端校验与最佳实践
- [[hhs/gRPC/1. Protobuf 基础篇/05-序列化与跨语言实战]] — wire encoding 深入 + Java/Python/Go 跨语言互调踩坑记录