一个类 Clojure 运行时,如果能打成约 10MB 单文件,冷启动约 7ms,听起来很容易被解读成“JVM Clojure 终于有轻量替代了”。

let-go 有意思的地方也在这里:它确实轻,但项目方没有把话说满。README 对它的定义是用 Go 编写的类 Clojure 字节码编译器和 VM,closely resembling Clojure,而不是 clojure/clojure 的 drop-in replacement。

这个边界比 7ms 更重要。

我更愿意把 let-go 看成一个面向脚本、嵌入和轻量分发的 Clojure 方言实验。它解决的是“我想用 Clojure 写法,但不想带 JVM 和完整 JDK”的问题,不是“我把现有 JVM Clojure 系统一键搬过去”的问题。

它解决的是分发成本,不是完整生态替换

let-go 的卖点很直接:小体积、快启动、单文件交付。

按 README 给出的基准,它的二进制约 10MB,冷启动约 7ms,空闲内存约 14MB。它还支持把程序编译成独立可执行文件、.lgb 字节码文件,或自包含的 WASM HTML 应用。

这对两类人最有用。

一类是 Clojure 开发者。过去写命令行工具、CI 脚本、小型自动化任务时,JVM 启动和 JDK 分发成本会让人犹豫。let-go 给了一个更轻的选项。

另一类是 Go 团队。团队如果想在 Go 程序里嵌入一门脚本语言,用来写配置逻辑、插件或内部 DSL,let-go 的 Go interop 会比“再塞一个 JVM”更顺手。

能力README 口径实际含义
体积约 10MB 二进制更适合 CLI、容器镜像和单文件分发
启动约 7ms 冷启动短命令、脚本任务更接近原生工具体验
内存约 14MB 空闲内存小型常驻进程和嵌入场景压力更低
输出形态可执行文件、.lgb、WASM HTML终端工具和浏览器实验都能覆盖

这里的动作建议很明确:如果你在写新 CLI、内部脚本、构建工具,可以试 let-go;如果你在维护大型 JVM Clojure 服务,不建议把它当迁移目标。

因为它现在最强的是“轻装上阵”,不是“全栈接管”。

兼容度不低,但尾部差异会决定能不能上生产

let-go 用 jank-lang 的 Clojure 跨方言测试套件做验证。README 里给出的数字是 4921 个断言中通过 4696 个,约 95.4%。

这个成绩说明它已经覆盖了不少日常 Clojure 语义。包括宏、解构、协议、records、多方法、transducers、lazy seqs、持久化数据结构和 BigInt。

它还带了不少实用能力:core.async、HTTP server/client、JSON、Transit、IO、Babashka pods、nREPL、Go interop。

nREPL 对 CIDER、Calva、Conjure 这类编辑器工作流有意义。Go interop 则让 Go struct 可以映射到 let-go records,并支持 Go 与 let-go 函数互调。

但缺口也要放在台面上。

README 列出的未实现或行为差异包括 STM/Refs、Agents、Spec、deftype、reify、reader tagged literals、部分数值溢出行为、BigInt 边界提升,以及数值塔和 BigDecimal 行为限制。文档中对 BigDecimal 的表述并不完全一致,稳妥读法是:任意精度十进制和舍入控制还不能按 JVM Clojure 预期使用。

这会影响谁?主要是两类项目。

一类是依赖 JVM Clojure 完整语义的业务代码。尤其用了 Spec、Refs、Agents、复杂数值逻辑的项目,暂时不适合迁移。

另一类是准备把 let-go 嵌进 Go 产品的团队。可以先用在插件、规则、配置表达式这类低风险边界里,不要一开始就放到核心交易、计费或强一致状态逻辑里。

要不要试,不看宣传语,看检查项:

检查项适合试用建议延后
代码形态新脚本、新 CLI、小型工具现有大型 JVM Clojure 系统迁移
语义依赖宏、协议、records、transducers 等常见能力STM/Refs、Agents、Spec、deftype、reify
数值要求普通整数、BigInt 基础使用严格 BigDecimal、溢出和舍入语义
集成方式Go 嵌入、轻量 DSL、内部插件依赖完整 JVM 生态和成熟库链

这不是泼冷水。恰恰相反,边界说清楚,工具才有用。

它像 Babashka 的邻居,不像 JVM Clojure 的继任者

let-go 最容易被拿来和 JVM Clojure、Babashka、Joker 比。

项目方在 Apple M1 Pro 上给出了一些 benchmark,显示 let-go 在体积、启动和若干短任务上有优势。但这些数字来自 README 的测试环境和样例,只能说明项目方口径下的表现,不能外推成所有业务负载的通用结论。

更合理的看法是看定位差异。

运行时更像什么看 let-go 时要注意什么
JVM Clojure官方主线,生态最完整let-go 更轻,但不是完整替代
Babashka面向脚本的 Clojure 运行时let-go 支持 Babashka pods,不等于完整 Babashka 兼容
JokerGo 写的 Clojure 方言解释器let-go 走字节码 VM 路线,更强调启动和执行形态

所以,真正该观察的不是“它能不能打败谁”,而是三个更具体的问题。

第一,Clojure 兼容测试的尾部差异能不能继续缩小。95.4% 已经能说明覆盖面,但生产系统往往死在剩下那一小截。

第二,Go interop 能不能稳定处理类型映射、错误传播和调试体验。嵌入式语言最怕“能跑 demo,难排线上问题”。

第三,WASM 和 nREPL 能不能从演示能力变成可维护工作流。能生成自包含 WASM HTML 很酷,但团队真正关心的是调试、构建、依赖和版本管理。

如果这三件事进展顺,let-go 会成为 Clojure 开发者做小工具、Go 团队做嵌入 DSL 的新选择。如果进展慢,它也仍然可以是一个干净的实验运行时,但不该被包装成 JVM Clojure 的替身。

回到开头那个 7ms。这个数字吸引人,但它不是结论。let-go 的价值在于把 Clojure 风格塞进了 Go 的轻分发模型;它的风险也在这里:轻是优点,越位才是问题。