一个类 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 兼容 |
| Joker | Go 写的 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 的轻分发模型;它的风险也在这里:轻是优点,越位才是问题。
