trim 是 dynamicgo 中的一个核心的数据处理包,主要提供对象级别的数据裁剪(Pruning / Fetch)与按需赋值(Assign)的能力。其设计初衷是能以一套统一的 DSL (描述符) 高效处理嵌套层次较深的复杂 Go 结构体(特别是通过 Protobuf 和 Thrift 自动生成的结构化数据),实现按需保留所需字段、并将其逆向赋值组回的功能。
trim 包围绕三个核心抽象展开:
- Descriptor(描述符):通过树状结构描述所需保留或访问的特定路径与字段(类似于 GraphQL 的选择树)。
- Fetcher(提取器):顺着 Descriptor 描述的路径,以反射的方式将源(结构体 / Map / List 等)中特定数据抽取出来。
- Assigner(赋值器):提取过程的逆向应用——根据 Descriptor 的树状结构,将零散结构化数据(如通过 Fetch 得到的嵌套 Map/Slice)自动精准映射赋值到目标强类型 Go 对象中,并专门处理了 Protobuf 的未知字段。
位于 desc.go。
- 设计:由特定的类型种类
Kind(支持Leaf/Struct/StrMap/List) 和一组Children字段组成。例如描述提取字典的键或对应的 ID 字段。 - 循环引用支持:提供了
Normalize()方法(此方法被调用后 Descriptor 才会变为可用状态)。在其内部会对具有指向自身(或者内部构成环)的图状描述符网络进行缓存和解析,防脱轨或无尽循环。 - 序列化监控:内置
String()和MarshalJSON()支持以文本格式探查复杂 Descriptor 的全貌。
位于 fetch.go。
- 底层机制:基于
reflect实施深层对象的遍历。 - 高速缓存:对于 Thrift 或 Protobuf 结构体,
Fetcher不会每次都动态解析 struct tag。它全局维护了一个fieldCache字典 (sync.Map),一次解析字段与 Tag 之后(包括解析 Protobuf Name、Thrift Field ID 等),就将结构体的索引记录到缓存,之后以 O(1) 的开销命中字段的reflect.Value。 - 未知字段提取:内置对 thrift
_unknownFields等未知域字段的读取兼容,以防止在降级/动态处理时漏掉非强类型的字段。可以使用SetThriftUnknownFieldName()灵活重定义配置。
位于 assign.go。
- 尽力而为(try-best mode)策略:赋值器遇到单条字段的类型不匹配或缺失时,不仅不会立刻中断进程,还会将其记录进一个
errorCollector中并继续执行,最后通过MultiErrors打包返回所有的错误(或者可通过AssignOptions.DisallowNotDefined提供更严苛的拦截策略)。 - Protobuf Unknown Fields 兜底:如果在源数据中提取到了不在实际 Go
struct定义内的字段(例如高版本的 protobuf 数据赋值降级到低版本的 Go 结构体):- 若相应的未知字段名存在于 Descriptor 的元数据内,Assigner 会将数据序列化为 protobuf Binary wire 结构并放至目标结构体的
XXX_unrecognized字段。 - 对于未分类(Unkeyed)字面量,支持注入至
XXX_NoUnkeyedLiteral(也可以由SetPB***FieldName函数重新指定)。
- 若相应的未知字段名存在于 Descriptor 的元数据内,Assigner 会将数据序列化为 protobuf Binary wire 结构并放至目标结构体的
- 零分配和对象池化(对象复用):为了在递归过程中降低性能损耗,在栈内跟踪 (Path stack / frames) 广泛使用了资源池(
sync.Pool)复用,减少了不必要的 GC 开销。
package main
import (
"fmt"
"github.com/cloudwego/dynamicgo/trim"
)
// 以一个生成的 Protobuf 消息结构体为例
type User struct {
ID int `protobuf:"varint,1,req,name=id"`
Name string `protobuf:"bytes,2,opt,name=name"`
}
func main() {
// 1. 构建数据抓取描述符 Descriptor: 这里意图仅保留 ID
desc := &trim.Descriptor{
Kind: trim.TypeKind_Struct,
Type: "User",
Children: []trim.Field{
{Name: "id", ID: 1, Desc: &trim.Descriptor{Kind: trim.TypeKind_Leaf}},
},
}
// WARNING: 构建完毕必须调用 Normalize() 处理树的搜索索引
desc.Normalize()
// 原始需要被裁剪的数据
user := &User{ID: 10086, Name: "Alice"}
// 2. 使用 FetchAny (裁剪)
fetcher := trim.Fetcher{}
fetchedResult, err := fetcher.FetchAny(desc, user)
if err != nil {
panic(err)
}
// fetchedResult 中目前仅包含要求的 ID 字段了
// 输出: Fetch 结果: map[id:10086]
fmt.Printf("Fetch 结果: %+v\n", fetchedResult)
// 3. 使用 AssignAny (重新映射到空数据体)
newUser := &User{}
assigner := trim.Assigner{}
// 从前一级裁切获得的 object (如 map) 填入到 newUser 结构中
err = assigner.AssignAny(desc, fetchedResult, newUser)
if err != nil {
panic(err)
}
// newUser 被赋予了部分值
// 输出: Assign 结果: &{ID:10086 Name:}
fmt.Printf("Assign 结果: %+v\n", newUser)
}