深入解析 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),仅供参考

相关新闻

烘焙后城市场景满是黑斑?用6步检查 Lightmap UV 与光照接缝

烘焙后城市场景满是黑斑?用6步检查 Lightmap UV 与光照接缝

城市场景完成光照烘焙后,如果出现整面发黑、局部脏斑、模块接缝发亮,先不要急着提高灯光强度。更常见的原因是 Lightmap UV 重叠、UV 岛间距不足、光照贴图分辨率与对象尺寸不匹配,以及薄面、法线或模块边界存在问题。 本文用一个最小场景演…

2026/9/24 13:36:13 阅读更多 →
不依赖库!C# 手搓 Modbus RTU 主站读取变送器数据(串口 + WinForms 实战,含帧构造、CRC16、数据解析)

不依赖库!C# 手搓 Modbus RTU 主站读取变送器数据(串口 + WinForms 实战,含帧构造、CRC16、数据解析)

不依赖库!C# 手搓 Modbus RTU 主站读取变送器数据(串口 WinForms 实战,含帧构造、CRC16、数据解析)很多人做 Modbus 通信直接用 NModbus 之类的库,帧怎么拼、CRC 怎么算一概不知,一旦通信出问题就无从下手…

2026/9/24 13:36:13 阅读更多 →
热加载为什么难——卸载 DLL 的四个前提

热加载为什么难——卸载 DLL 的四个前提

进入阶段三。前面的内容,哪怕你一句都没写对,顶多是功能不对、偶尔崩溃。这一阶段的主题是:不停机把正在用的插件换掉。做错了,是进程直接没了。 先说一个反直觉的事实,也是我当年卡了一整周的地方:QPluginLoader::unload() 你调它,它十有八九返回 false。而且这不是你…

2026/9/24 13:36:13 阅读更多 →

最新新闻

如何优雅处理“AI bs”:从需求澄清到架构隔离的完整指南

如何优雅处理“AI bs”:从需求澄清到架构隔离的完整指南

你正在写一个无关紧要的配置模块,经理从线上开会回来,丢下一句"我们得在这个版本里把AI加上"。你问加什么AI、解决什么问题、给谁用,经理说"就是那种AI,你懂的,别人都有了,我们不能落后&quo…

2026/9/24 21:34:32 阅读更多 →
ZooKeeper投票五元组深度解析:从选举原理到故障排查

ZooKeeper投票五元组深度解析:从选举原理到故障排查

1. 从一次诡异的集群故障说起先说个真实案例。有一次我在测试环境搭了一套三节点的 ZooKeeper 集群,版本是 3.5.7,机器配置都正常,网络也通。启动之后我例行检查了一下状态,发现 leader 节点一直不稳定,隔几分钟就重新…

2026/9/24 21:34:32 阅读更多 →
交换机路由器配置实战:从Console到业务通的全链路解析

交换机路由器配置实战:从Console到业务通的全链路解析

1. 为什么“交换机、路由器配置”不是一句空话,而是网络工程师每天要拆解的活儿你有没有遇到过这样的场景:刚接手一台新到的华为S5720交换机,连上Console线,敲完system-view,手却停在了那里——接下来该输什么&#xf…

2026/9/24 21:34:32 阅读更多 →
中文命名实体识别实战:BERT+BiLSTM+CRF技术栈详解

中文命名实体识别实战:BERT+BiLSTM+CRF技术栈详解

简介:这是一份基于BERTBiLSTMCRF实现中文命名实体识别的Python课程设计源码,主要面向需要完成NLP方向课程设计、期末大作业或毕业设计的本专科学生。项目实现了从原始语料处理、字符编码、BERT向量表征、BiLSTM特征提取到CRF序列解码的完整NER流程&#…

2026/9/24 21:34:32 阅读更多 →
OpenWiki 实战:本地 Markdown 知识库与 AI Agent 集成指南

OpenWiki 实战:本地 Markdown 知识库与 AI Agent 集成指南

1. 从命令行到知识库:OpenWiki 到底解决了什么问题第一次听说 OpenWiki 是在一个做 AI Agent 开发的朋友群里,有人甩了张截图:终端里敲一行命令,本地的 Markdown 文件夹瞬间变成一套可检索、可对话的知识库,还能直接挂…

2026/9/24 21:34:32 阅读更多 →
Uni LLM Bench:自托管LLM API基准测试平台实战指南

Uni LLM Bench:自托管LLM API基准测试平台实战指南

1. 为什么要自己做一套 LLM API 基准测试平台先说个真实场景。我们团队做多租户平台,上游接了好几家大模型 API,有官方的,也有走聚合网关的。上个月某个渠道换了底层模型,线上监控没做细,等业务方反馈"回答变慢了…

2026/9/24 21:33:32 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →