夜雨聆风学习资料网

ARTICLE · 1109518

Go 插件化总写崩?这个运行时连卸载都替你做了

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 工具编排、配置热更新的朋友,这篇建议先存着。

先说一句:这篇偏工程向,写给正在做 Go 插件化、Agent 工具编排或配置热更新的开发者。不写代码也能看懂思路,看代码的那几段可以直接抄。 

1. 速览:它把「可组合」拆成了两个维度

一句话概括:你只声明「我要什么」和「我怎么创建」,创建与拆掉的时机交给运行时。

时间维
每个副作用都携带显式的逆操作,由运行时追踪;组件卸载时环境被完全还原。
空间维
组件以协效应声明对服务的依赖;依赖满足状态一变,运行时自动驱动加载与卸载。

为什么叫「时空」可组合?论文把组件系统拆成了两个正交维度:时间——组件有生有死,死了必须把环境还原干净;空间——组件之间有依赖,依赖断了必须自动退场。多数框架只管其中一半,另一半靠人自觉,cordis 是用一套运行时同时管两半,而且互不耦合。

图:cordis 的三层架构(宿主层 / 运行时层 / 声明式配置层)

对照到 Go 类型上:Plugin 是组件定义,Fiber 是组件实例,Context 是链式上下文,Reflect 存协效应,Loader 负责声明式配置。

2. 时间维:每个副作用,自带一把关掉的钥匙

常见做法:只写 Init 不写 Close,卸载靠人肉记忆;漏一个连接、一个 goroutine,就是一颗延迟引爆的雷。 
正确姿势:把副作用登记给运行时,并交出它的逆操作。连接、监听端口、临时文件、后台协程,全都登记在案,卸载时按 LIFO 逆序自动回收。 
_, err := ctx.Effect("conn", func() (cordis.Dispose, error) {    return func() { log.Println("close db") }, nil})

两个细节很见功力:· Dispose 要求幂等,关两次也不出错;· 撤销遵循 dependant-first——先等依赖者下线,再销毁自己,不会出现「上游没了下游还在用」的空窗。

【能装上是能力,能拆干净才是工程】

3. 空间维:依赖声明出来,上下线全自动

常见做法:用一堆 if 判断依赖在不在,再手写监听、重试、降级——写的时候费劲,改的时候更容易漏。 
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. 热更新与失败恢复:改个数字就换实例

热更新的难点从来不是速度,是顺序:旧的没退干净,新的就起不来。

 示例里把 Web 服务从 8080 改到 9090:旧实例先 shutdown,新实例再 listen,应用不用重启。配置没过校验时,组件进入 failed 状态但原地保留,改对了自动恢复——不会出现「改错一个配置,整个模块人间蒸发」的事故。 

图: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 的单线程事件循环:全部状态转换在单一调度器里串行执行。

· 你的回调(Apply / Dispose / 事件监听)天然跑在调度器里,操作 Context 上任何 API 都不用加锁;· 外部 goroutine 一律走 App.Do(异步)或 App.DoSync(同步)进场;· 任务队列是无界 slice + 互斥锁,单个任务里继续投递任务不会把自己堵死。一个要记住的坑:别在调度器回调里调 DoSync 或 Wait,会死锁。 

【免锁不靠运气,靠的是「只有一个 goroutine 改状态」】

8. 工程质量:它拿什么证明自己靠谱

很多开源项目的 README 只写「好用」,这个写的是「我怎么证明好用」。

·44 项测试全绿,覆盖核心运行时与声明式配置层,另含白盒不变量与 5 个基准;CI 在 Go 1.22 与 stable 两档上跑 -race 与冒烟。

· 挑几个测试名你就知道它在意什么:8 个 goroutine 混合 Do/DoSync(任务不丢不重、调度严格串行)、并发注册 160 个实例全部收敛为 active、1100 个入口单次 Load 不死锁、Close 与外部投递并发无死锁且幂等。

· 基准数据也给了:存活集放大 64 倍,单次更替的摊还成本只涨到 2.2 倍——说明它没有把历史操作量攒成包袱,长期跑不会越跑越慢。

【判断一个库能不能上生产,先看它有没有为难自己】

9. 上手三步,以及什么情况别用

先跑通,再读码,然后才谈接入。

第一步 拉依赖:go get github.com/corecraft-io/cordis第二步 看效果:go run ./example,端到端示例会依次打印五个场景(初始加载、热重载、提供者下线、多租户双栈、动态移除)的状态流,比读文档快。第三步 本地联调:改动要和依赖它的项目一起验证时,用 go.work,别用 replace——你的模块被别人当依赖时 replace 会被忽略,还会弄坏 go install。 

也说实话,这几类场景别上:一次性脚本、小 CLI 工具、两三个包的玩具项目。架构是给会长大的系统准备的,不是给每段代码准备的。

【先让它替你管卸载,再谈让它替你管架构】

顺手说一句

想读源码又怕硬啃的,把 cordis.go 顶部的包注释和 README 第 2 节的概念对照表先喂给 DeepSeek、通义千问这类国产大模型,让它把调用链画出来,再回头跑一遍 example,半天就能建立整体感。

三个使用小贴士

① 上手顺序 先跑 example 看五个场景的输出,再回头读 README 第 2 节的概念对照表。

② 本地联调 改 cordis 又要联调下游项目时,用 go.work,不要用 replace。

③ 读码提速 把包注释喂给 DeepSeek、通义千问这类国产大模型,让它先给你画调用链。

你现在的插件系统,卸载的时候靠什么把资源关干净?评论区聊聊你的踩坑经历。

相关学习资料