ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

深入解析 lann/builder:用 Go 编写不可变、可复用的流式 Builder DSL

深入解析 lann/builder:用 Go 编写不可变、可复用的流式 Builder DSL 人工智能AI AgentAgent 沙箱云原生容器运行时零信任【免费下载链接】substrateAgent Substrate: the core system项目地址https://gitcode.com/GitHub_Trending/substrate7/substrate点击查看免费下载Builder 是 Go 语言中一套面向“流式fluent不可变构建器”的底层工具库本仓库将其以 vendor 方式内置在vendor/github.com/lann/builder并同时携带其持久化数据结构依赖vendor/github.com/lann/ps。读完本文你将掌握 Builder 的核心 APISet、Append、Extend、Get、GetStruct等、注册机制Register/RegisterType、底层不可变数据结构的实现原理以及它如何支撑起 Squirrel 这类流式 SQL 生成器——并且可以立即在自己的库中复刻同样的模式。一、Builder 要解决什么问题在 Go 里当我们想让 API 调用者以“链式调用”的方式配置一个复杂对象时最自然的写法是resp : ReqBuilder. Url(http://golang.org). Header(User-Agent, Builder). Get()这种风格被称为 fluent DSL。它的问题在于如果每一步都直接修改同一个内部结构体那么中间状态会被破坏——比如两个调用者共享同一个build : WordBuilder.AddLetters(Build)其中一个继续追加er另一个追加ing如果结构体是可变的两者就会互相污染。Builder 的核心主张是每一步链式调用都返回一个全新的、与原状态共享底层结构的新实例从而实现“每个中间步骤都可以安全复用”build : WordBuilder.AddLetters(Build) builder : build.AddLetters(er) building : build.AddLetters(ing)上面的例子中builder得到Builderbuilding得到Building而build自身仍然是Build——这正是“不可变immutable”语义的价值。二、不可变的基石lann/ps 持久化数据结构“不可变”不是靠每次全量拷贝实现的那会带来 O(N) 的复制开销Builder 选择的是持久化数据结构persistent data structures。其依赖来自vendor/github.com/lann/ps该包是github.com/mndrix/ps的稳定 fork见 vendor/github.com/lann/ps/README.md。ps.Map是一个字符串到任意值的持久化关联数组接口定义在 vendor/github.com/lann/ps/map.goSet(key, value)返回新 map不修改原 mapO(log N)Delete(key)返回移除该键后的新 mapO(log N)Lookup(key)返回(value, bool)O(log N)Size()O(1) 返回键值对数量ForEach(f)、Keys()遍历辅助。从实现上看ps.Map是一棵路径拷贝path-copying哈希树每个节点固定拥有 8 个子树childCount 8键通过 FNV-1a 哈希见hashKey逐 3 位分片shiftSize 3向下路由Set时只克隆从根到叶子的那条路径上的节点setLowLevel中的m : self.clone()其余子树与原树共享因此时间与空间开销都与树高成正比而不是与整个 map 的大小成正比。空树nilMap的所有子树都指向自身从而消除了全部空指针。ps.List则是一个持久化单向链表vendor/github.com/lann/ps/list.goCons(val)以 O(1) 代价在头部插入新节点并返回新链表新节点共享原链表作为尾部nilList作为所有空链表的共享尾部。注意它是头插法因此 Builder 在把 list 还原成 slice 时会倒序回填见下文Get部分。三、核心数据结构与基础操作Builder本体定义在 vendor/github.com/lann/builder/builder.gotype Builder struct { builderMap ps.Map }它内部只持有一个ps.Map所有命名值都存在这个 map 里。包级变量EmptyBuilder是唯一的空构建器起点var ( EmptyBuilder Builder{ps.NewMap()} emptyBuilderValue reflect.ValueOf(EmptyBuilder) )3.1 Set 与 Delete写入与移除命名值func Set(builder interface{}, name string, v interface{}) interface{} func Delete(builder interface{}, name string) interface{}Set调用ps.Map.Set得到新 map再包装成新的Builder并通过reflect.Value.Convert转换回调用者的自定义 builder 类型返回convert定义在 vendor/github.com/lann/builder/reflect.go。因此原 builder 完全不变返回的是副本。Delete同理用于移除某个命名值。源码注释明确约定所有接收 builder 的函数若底层类型不是Builder会直接 panic。3.2 Append 与 Extend追加列表值func Append(builder interface{}, name string, vs ...interface{}) interface{} func Extend(builder interface{}, name string, vs interface{}) interface{}Append本质是Extend的变参形式将多个值追加到命名列表Extend则接受任意类型的 slice/array通过reflect.ValueOf(vs).Len()遍历见forEach。两者的内部逻辑builder.go若传入值为 nil直接返回原 builder从 map 中查找该名字对应的ps.List若不存在或类型不是ps.List则新建空列表逐个Cons新值头插用Set写回新 map。由于是头插元素在内部是逆序存储的最终输出时会统一反转。3.3 Get 与 GetMap读取构建结果func Get(builder interface{}, name string) (interface{}, bool) func GetMap(builder interface{}) map[string]interface{}Get返回单个命名值若该值是用Append/Extend写入的ps.List则会调用listToSlice把它转换成 slicebuilder.go从size-1倒序回填把链表的头插顺序还原为追加顺序。默认 slice 类型是[]interface{}如果该名字是已注册结构体的导出字段slice 会被转成对应字段的类型如[]string。GetMap则一次性返回所有命名值的map[string]interface{}。四、注册机制把 Builder 变成结构体工厂4.1 Register / RegisterType要让GetStruct工作必须先把 builder 类型与目标结构体类型“注册”起来。注册逻辑在 vendor/github.com/lann/builder/registry.gofunc RegisterType(builderType reflect.Type, structType reflect.Type) *reflect.Value func Register(builderProto, structProto interface{}) interface{}RegisterType内部用sync.RWMutex保护的registry map[reflect.Type]reflect.Type记录映射并会调用structType.NumField()来确保传入的确实是结构体类型否则 panicRegister是RegisterType的便捷包装传入两个实例返回一个可作链式起点的空 builder 实例底层是EmptyBuilder转换而成。4.2 GetStruct / GetStructLike从 builder 装配结构体func GetStruct(builder interface{}) interface{} func GetStructLike(builder interface{}, strct interface{}) interface{}两者都通过scanStructbuilder.go完成装配遍历 builder 中所有命名值只处理名字以大写字母开头ast.IsExported即“如果它是标识符就属于导出”的值按名字匹配结构体字段对于ps.List直接listToSlice成对应字段类型对于nil仅当字段类型为 chan/func/interface/map/ptr/slice 之一时置零值否则field.Set会 panic其余值直接reflect.ValueOf后field.Set。GetStruct要求该 builder 类型已经注册否则返回 nilGetStructLike则不必注册直接以传入的strct实例的类型为目标。五、实战用 10 行代码定义自己的流式 Builder以下是原 README 的完整示例已随仓库 vendor 在 vendor/github.com/lann/builder/README.md它演示了定义 builder 的完整套路——声明一个底层类型为builder.Builder的新类型然后在方法里调用包级函数并做类型断言import github.com/lann/builder type Muppet struct { Name string Friends []string } type muppetBuilder builder.Builder func (b muppetBuilder) Name(name string) muppetBuilder { return builder.Set(b, Name, name).(muppetBuilder) } func (b muppetBuilder) AddFriend(friend string) muppetBuilder { return builder.Append(b, Friends, friend).(muppetBuilder) } func (b muppetBuilder) Build() Muppet { return builder.GetStruct(b).(Muppet) } var MuppetBuilder builder.Register(muppetBuilder{}, Muppet{}).(muppetBuilder)使用效果MuppetBuilder. Name(Beaker). AddFriend(Dr. Honeydew). Build() Muppet{Name:Beaker, Friends:[]string{Dr. Honeydew}}拆解这段套路type muppetBuilder builder.Builder让自定义类型拥有Builder的底层布局从而可以被包级函数接收并转换每个 setter 返回muppetBuilderbuilder.Set/builder.Append返回interface{}必须断言回具体类型这是 fluent 链能够继续下去的关键Build()调用builder.GetStruct借助注册表把命名值装配进Muppet结构体builder.Register(muppetBuilder{}, Muppet{})完成类型注册并生成链式起点注意Friends是[]string而Append写入的是ps.List最终GetStruct会依据注册的字段类型把它还原成[]string——这正是“注册”这一步骤必不可少的原因。AddFriend的多次调用会不断追加AddFriend(A).AddFriend(B)最终得到Friends: []string{A, B}。每次Append都产生新 map中间状态可自由复用天然规避了可变结构体共享带来的 bug。六、真实世界的范例Squirrel 流式 SQL 生成器README 明确指出Builder 最初就是为Squirrel——一个流式 SQL 生成器——而写的是它最典型的使用案例。本仓库的 vendor 目录中恰好完整保留了 Squirrelvendor/github.com/Masterminds/squirrel/可以直接对照学习。以 vendor/github.com/Masterminds/squirrel/squirrel.go 为例Squirrel 内部正是通过builder.Set存储RunWith等配置项return builder.Set(b, RunWith, runner)而在 vendor/github.com/Masterminds/squirrel/select.go、vendor/github.com/Masterminds/squirrel/insert.go、vendor/github.com/Masterminds/squirrel/update.go、vendor/github.com/Masterminds/squirrel/delete.go 以及各自的_ctx.go变体中处处可见builder.Set、builder.Append、builder.GetStruct的身影。Squirrel 的典型用法users : sq.Select(*).From(users).Where(sq.Eq{name: Beaker})Select(...)返回的SelectBuilder本质上就是一个注册过的 builder 类型Where、From、Join等每步都返回新实例最终ToSql()内部调用builder.GetStruct取出完整状态并渲染成 SQL。这意味着中间任意一步都可以保存下来、分支复用——比如基础查询对象被多个场景追加不同的过滤条件。七、使用注意事项与约束从源码中可以提炼出以下几条明确约束见各函数注释底层类型必须是 BuilderSet、Get、GetStruct等函数若收到底层类型不是Builder的值会 panic自定义 builder 类型必须用type X builder.Builder声明。导出字段才生效GetStruct/GetStructLike只把名字以大写字母开头的命名值写入结构体对应字段小写开头的命名值会被忽略。类型不匹配会 panic若某命名值无法赋值给注册结构体的对应字段如把字符串赋给 int 字段field.Set会 panicnil值也只对 chan/func/interface/map/ptr/slice 这类可置零的字段合法。不可变性的边界Builder 本身不可变但放入的值若本身是可变对象如*bytes.Buffer仍需使用者自己保证不在使用期间被修改——源码注释对此有明确提醒。注册是全局的registry是包级 mapRegister后全局生效同一 builder 类型不可重复注册到不同结构体。八、许可证Builder 采用 MIT License 发布见 vendor/github.com/lann/builder/LICENSE其依赖lann/ps同样为 MIT 许可vendor/github.com/lann/ps/LICENSE可放心在商业项目中集成使用。小结lann/builder用约两百行核心代码把“流式调用 不可变中间态 反射装配结构体”三件事封装成了清晰的小型 APIlann/ps提供持久化 map/list 作为不可变基石Set/Append/Extend负责写入Get/GetMap/GetStruct负责读取与装配Register负责建立 builder 与结构体之间的类型映射。掌握它之后你既能读懂 Squirrel 的整套 fluent SQL 实现也能在 10 行代码内为自己的库定制同样优雅的链式 DSL。赞分享人工智能AI AgentAgent 沙箱云原生容器运行时零信任【免费下载链接】substrateAgent Substrate: the core system项目地址https://gitcode.com/GitHub_Trending/substrate7/substrate点击查看免费下载相关推荐深入解析 lann/builder为 Go 库构建不可变链式 DSL 的通用基础设施深入解析 lann/builder为 Go 库构建不可变链式 DSL 的通用基础设施 导读 lann/builder 是一个专为 Go 语言设计的通用「构建器后端云原生容器编排微服务Cilium 仓库中的 Go 流式不可变 Builder 库lann/builder源码级解析Cilium 仓库中的 Go 流式不可变 Builder 库lann/builder源码级解析 导读 vendor/github.com/lann/buil云原生网络服务网格可观测性网络安全eBPFKubeSphere 依赖树中的 Go 流式不可变构建器lann/builder 源码精读KubeSphere 依赖树中的 Go 流式不可变构建器lann/builder 源码精读 本篇以 KubeSphere 仓库 vendor 目录中引入的 l云原生容器编排后端微服务多集群DevOps可观测性AI 技能上一篇three.js TubeGeometry 详解沿 3D 曲线扫掠生成管道网格几何体下一篇Bench更强大的命令行基准测试工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表