From d3216cb3b48023517bd427a0a6fca287cc0a7e88 Mon Sep 17 00:00:00 2001 From: what Date: Fri, 28 Aug 2026 20:05:13 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=AF=B4=E6=98=8E=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E7=AC=A6=E7=BC=93=E5=AD=98=E7=9A=84=E7=94=9F?= =?UTF-8?q?=E5=91=BD=E5=91=A8=E6=9C=9F=E4=B8=8E=20reflect.StructOf=20?= =?UTF-8?q?=E7=9A=84=E6=B3=A8=E6=84=8F=E4=BA=8B=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充两个评审/接手时大概率会问到的问题: 1. 缓存什么时候清除 —— 永不清除, 正常情况下也不需要。key 是 reflect.Type, 程序里声明的类型是编译期确定的有限集合, 缓存大小 warmup 之后收敛。 附实测占用(1000 个类型约 0.6 MB), 并说明只增不删是有意设计: 描述符指针永久有效, 递归引用不需要生命周期管理。 2. 唯一的例外是调用方用 reflect.StructOf 在运行时动态造类型(典型是 ORM 按 查询字段拼扫描目标 struct)。实测 12 字段的动态类型每个约 3315 字节, 其中 reflux 占 59%、Go runtime 自己的类型元数据占 41% —— 后者同样永不回收, 所以这不是 reflux 特有的问题。 同时给出两条实践建议: 优先在上游收敛字段组合并缓存类型(reflect.StructOf 即使命中 runtime 缓存也要 2μs / 30 次分配); 以及注意字段顺序 —— 字段集合相同但顺序不同会产生不同的类型, 拼字段时遍历 map 会因为迭代顺序 随机而每次造出新类型, 那才是真正会导致无界增长的 bug。 --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/README.md b/README.md index 0e35daa..a1d8479 100644 --- a/README.md +++ b/README.md @@ -1251,6 +1251,62 @@ type Reflux interface { `Get` 剩下的 2 次分配是 API 形状决定的下限: 一次是返回的 `R` 包装对象(24 字节), 一次是可变参数切片(通过接口调用时逃逸分析穿不透)。想完全避免就用 `Get[T]`。 +### 类型描述符缓存:什么时候清除 + +**永不清除,正常情况下也不需要清除。** + +缓存的 key 是 `reflect.Type`。程序里**声明**的类型是编译期确定的有限集合, +runtime 为它们创建的 `reflect.Type` 本身就是永久对象,所以缓存大小收敛于 +"程序实际用到的类型数",warmup 之后不再增长。 + +实测占用(描述符 + 字段切片 + 字段名索引): + +| 类型数 | 缓存占用 | +|---|---| +| 100 | 约 0.06 MB | +| 1,000 | 约 0.6 MB | +| 10,000 | 约 6 MB | + +一个中等项目撑死几百个类型,占用在百 KB 级别。 + +设计上有意做成**只增不删**:描述符指针一旦发布就永久有效, +`Elem`/`Type` 这些递归引用不需要任何生命周期管理,热路径上也不必做 +引用计数或有效性校验。 + +#### 唯一的例外:`reflect.StructOf` 动态造类型 + +如果调用方用 `reflect.StructOf` / `reflect.MapOf` 等在**运行时动态生成 +形状各异的类型**,再把它们交给 `reflux.New`,缓存就会持续增长。 +典型场景是 ORM 按查询字段动态拼 struct 作为扫描目标。 + +实测(12 字段的动态 struct,1000 个): + +| | 每类型占用 | 占比 | +|---|---:|---:| +| Go runtime 的类型元数据 | 1369 B | 41% | +| reflux 的类型描述符 | **1945 B** | **59%** | +| 合计 | 3315 B | | + +两点需要说明: + +1. **这不是 reflux 特有的问题**。`reflect.StructOf` 创建的类型 + **Go runtime 自己也永不回收**,泄漏在更底层就已经发生了; + reflux 是在此基础上多加了约 59%。 +2. **类型数量通常仍然有界**。`reflect.StructOf` 对**相同字段集合**(名称、 + 类型、tag、**顺序**都相同)返回同一个 `reflect.Type`,所以重复调用不会造新类型。 + 只有当字段组合本身是无界的(比如允许客户端任意指定 `?fields=a,b,c`), + 才会真正持续增长。 + +如果你的场景确实会动态生成大量不同的 struct 类型,建议: + +- **优先在上游收敛**:把字段组合固定成有限的几种,或者在构造类型的那一层 + 加缓存(既省掉 `reflect.StructOf` 的开销,也自然限住了类型数量)。 + 顺带一提,`reflect.StructOf` 即使命中 runtime 自己的类型缓存也要 + **2μs 上下、30 次左右的分配**,重复调用本身就值得避免。 +- **注意字段顺序**:字段集合相同但顺序不同会产生**不同**的类型。 + 如果拼字段时遍历了 map(Go 的 map 迭代顺序随机),同一个逻辑查询每次都会 + 造出新类型 —— 这是真正会导致无界增长的 bug,务必用切片保证顺序稳定。 + ## 注意事项 1. **指针 vs 值传递**: