ARTICLE · 1109518
Go 插件化总写崩?这个运行时连卸载都替你做了
AI 知识应用 · 开源工具
上周五跟一个做 AI 应用的朋友吃饭。他负责的那个后台,插件越加越多:接数据源的、做工具调用的、对接各家大模型的,全塞在同一个进程里。
我问他:「升级一次得多麻烦?」他苦笑:「改个配置而已。可上回旧实例的端口没退干净,新服务死活起不来,从晚上九点排查到凌晨。」
我又问了一句更扎心的:「那插件卸载的时候,谁负责把它开过的连接、占过的端口、起的协程,一个个关回去?」
他想了半天,憋出两个字:「靠自觉。」
问题从来不是「插件能不能装上」,而是「插件能不能被干净地拆掉」。
今天就介绍一个把「卸载」当一等公民的开源项目——cordis 的纯 Go 实现。它的理论来源是论文《A Programming Paradigm for Spatiotemporal Composability》(时空可组合组件模型,arXiv:2608.25512,北京大学与 DeepSeek-AI 联合署名);官方已有 TypeScript 版(npm 包名 cordis),这一版是逐模块对照语义之后的 Go 实现:零第三方依赖单 goroutine 免锁44 项测试全绿Apache-2.0
仓库地址:github.com/corecraft-io/cordis。做 Go 插件化、Agent 工具编排、配置热更新的朋友,这篇建议先存着。

1. 速览:它把「可组合」拆成了两个维度
一句话概括:你只声明「我要什么」和「我怎么创建」,创建与拆掉的时机交给运行时。
为什么叫「时空」可组合?论文把组件系统拆成了两个正交维度:时间——组件有生有死,死了必须把环境还原干净;空间——组件之间有依赖,依赖断了必须自动退场。多数框架只管其中一半,另一半靠人自觉,cordis 是用一套运行时同时管两半,而且互不耦合。

图:cordis 的三层架构(宿主层 / 运行时层 / 声明式配置层)
对照到 Go 类型上:Plugin 是组件定义,Fiber 是组件实例,Context 是链式上下文,Reflect 存协效应,Loader 负责声明式配置。
2. 时间维:每个副作用,自带一把关掉的钥匙
_, err := ctx.Effect("conn", func() (cordis.Dispose, error) { return func() { log.Println("close db") }, nil})两个细节很见功力:· Dispose 要求幂等,关两次也不出错;· 撤销遵循 dependant-first——先等依赖者下线,再销毁自己,不会出现「上游没了下游还在用」的空窗。
【能装上是能力,能拆干净才是工程】
3. 空间维:依赖声明出来,上下线全自动
cache := &cordis.Plugin{ Name: "cache", Inject: map[string]any{"database": nil}, // 协效应 Apply: func(ctx *cordis.Context, _ any) error { /* ... */ },}这就是「协效应」:依赖是声明出来的,不是轮询出来的。数据库提供者下线,cache 的效果被自动回收、回到 pending;提供者回归,自动恢复 active。全程不用写一行监听或重试代码。
还有个贴心设计:Provide 的第三个参数 check func() bool,用来表达「服务在,但暂时不可用」——每次依赖解析都会问它一句,返回 false 就当依赖没满足。
4. 热更新与失败恢复:改个数字就换实例
热更新的难点从来不是速度,是顺序:旧的没退干净,新的就起不来。

图:Fiber 生命周期状态机(failed 状态原地保留,改对配置即自动恢复)
【热更新的安全感,来自失败也有地方站着等】
5. 隔离域:一套代码,多套服务栈
多租户的老大难:同名服务要并存,还得互不干扰。
ctx.Isolate("database", "tenant-a") // 同域共享,跨域不可见示例跑了两套完整的「数据库 + 缓存 + Web」栈(tenant-a / tenant-b),移除其中一栈的数据库,另一栈纹丝不动。对得上的场景很具体:
· SaaS 多租户——每个租户一套服务栈,配置互不可见;
· 多智能体编排——一个进程里跑多套 Agent 配置,各用各的模型与工具;
· 灰度与 A/B——同一组件的两版实现并存,切换时按域走。
6. 声明式配置层:把组件树写成一份配置
实例化组件,从过程式代码变成一棵可协调的配置树。
loader.Load([]cordis.EntryOptions{ {ID: "db", Name: "database", Config: "postgres://prod"}, {ID: "web", Name: "web", Config: 8080},})做「后台可配置的插件市场」「工作流编排」这类需求,这一层基本是现成的底座:配置即拓扑,改一份配置就换一套组件组合。
【能用一份配置说清楚的组件,才配叫可运维】
7. 并发模型:一个调度 goroutine,回调免加锁
它复刻了 JavaScript 的单线程事件循环:全部状态转换在单一调度器里串行执行。
【免锁不靠运气,靠的是「只有一个 goroutine 改状态」】
8. 工程质量:它拿什么证明自己靠谱
很多开源项目的 README 只写「好用」,这个写的是「我怎么证明好用」。
·44 项测试全绿,覆盖核心运行时与声明式配置层,另含白盒不变量与 5 个基准;CI 在 Go 1.22 与 stable 两档上跑 -race 与冒烟。
· 挑几个测试名你就知道它在意什么:8 个 goroutine 混合 Do/DoSync(任务不丢不重、调度严格串行)、并发注册 160 个实例全部收敛为 active、1100 个入口单次 Load 不死锁、Close 与外部投递并发无死锁且幂等。
· 基准数据也给了:存活集放大 64 倍,单次更替的摊还成本只涨到 2.2 倍——说明它没有把历史操作量攒成包袱,长期跑不会越跑越慢。
【判断一个库能不能上生产,先看它有没有为难自己】
9. 上手三步,以及什么情况别用
先跑通,再读码,然后才谈接入。
也说实话,这几类场景别上:一次性脚本、小 CLI 工具、两三个包的玩具项目。架构是给会长大的系统准备的,不是给每段代码准备的。
【先让它替你管卸载,再谈让它替你管架构】
顺手说一句
想读源码又怕硬啃的,把 cordis.go 顶部的包注释和 README 第 2 节的概念对照表先喂给 DeepSeek、通义千问这类国产大模型,让它把调用链画出来,再回头跑一遍 example,半天就能建立整体感。
三个使用小贴士
① 上手顺序 先跑 example 看五个场景的输出,再回头读 README 第 2 节的概念对照表。
② 本地联调 改 cordis 又要联调下游项目时,用 go.work,不要用 replace。
③ 读码提速 把包注释喂给 DeepSeek、通义千问这类国产大模型,让它先给你画调用链。