let-go: 为 Go 设计的高性能 Clojure 方言

对于热爱 Clojure 的表达能力但又需要 Go 的部署便捷性的开发者来说,let-go 提供了一个极具吸引力的折中方案。它是一个字节码编译器和 VM,在 Go 中实现了一种 Clojure 方言,专为速度、小体积和易于分发而设计。

无论您是想构建轻量级的 CLI 工具、基于 WASM 的 Web 应用,还是为现有的 Go 应用提供脚本层,let-go 提供的运行时启动时间仅约为 7ms——这使其成为 JVM 甚至 GraalVM-native 二进制文件的极佳替代方案。

核心架构与性能

let-go 的核心是一个字节码 VM。通过将类 Clojure 的源代码编译为预编译的字节码格式 (.lgb),运行时在加载时跳过了读取器 (reader)、解析器 (parser) 和编译器阶段,从而实现了近乎瞬时的启动。

基准测试 vs. 其他运行时

与其他 Clojure 实现相比,性能提升最为明显。在 Apple M1 Pro 上,let-go 在体积和启动速度方面表现出显著优势:

指标 let-go Babashka Joker Clojure JVM
二进制文件大小 10MB 68MB 26MB 304MB (JDK)
启动时间 7ms 20ms 12ms
空闲内存 14MB 27MB 21MB 92MB

除了启动速度外,let-go 在计算密集型任务上也极具竞争力。例如,在斐波那契数列计算中,其性能接近 Babashka 的 96%,并且比使用树遍历解释器而非字节码 VM 的 Joker 快约 10 倍。

关键特性与能力

独立可执行文件与 WASM

let-go 的主要目标之一是消除对运行时的需求。用户可以使用 lg -b 将程序编译为单个、独立的二进制文件,该命令会将字节码打包进 lg 可执行文件中。

此外,let-go 支持编译为 WASM。通过 lg -w,程序会被编译成一个自包含的 HTML 页面。这包括通过 xterm.js 实现的全功能终端模拟,允许类 Clojure 程序直接在浏览器中运行,并提供原生般的终端体验。

Go 互操作性

let-go 旨在被嵌入使用。它允许 Go 开发者通过向 VM 暴露 Go 值和函数,将该语言作为脚本层来使用。关键的互操作特性包括:

  • 结构体映射 (Struct Mapping): Go 结构体可以被注册并映射到 let-go records,从而实现未变更数据的零成本往返。
  • 通道集成 (Channel Integration): Go channels (chan) 直接接入 let-gocore.async 实现,从而在两种语言之间实现无缝的事件管道传输。
  • 函数调用 (Function Calls): Go 函数可以直接从 let-go 代码中调用,反之亦然。

生态系统兼容性

为了保持与 Clojure 的高度兼容,let-go 通过了 clojure-test-suite 的测试,通过了超过 95% 的断言。它实现了核心命名空间,如 clojure.coreclojure.stringclojure.setcore.async

有趣的是,let-go 还支持 Babashka pods。这使得它能够访问庞大的独立程序生态系统,这些程序通过二进制协议暴露命名空间,从而无需重写库生态系统即可立即访问数据库、AWS 和 Docker 集成。

与标准 Clojure 的差异

虽然 let-go 旨在提供高保真度的体验,但它并不是 JVM Clojure 的直接替代品。开发者应当注意以下几个关键差异:

  • 并发 (Concurrency): 它缺少 Refs 和 STM (软件事务内存),尽管 atoms 和 channels 是完全支持的。
  • 数值行为 (Numeric Behavior): int64 上的数值溢出会静默地进行回绕 (wrap),而不是自动提升为 BigInt。用户必须使用显式的 BigInt 数学函数(例如,使用 +' 而不是 +)。
  • async 实现 (Async Implementation): 与 JVM 版本不同,let-go 中的 go 块是真正的 goroutines,而不是 IOC 状态机。这使得它们更廉价,但也允许它们直接调用阻塞操作。
  • 正则表达式 (Regex): 它使用 Go 的 re2 风格的正则表达式,而不是 Java 的。

入门指南

对于有兴趣尝试的用户,可以通过 Homebrew 或 Go 安装 let-go

brew tap nooga/let-go
brew install let-go

或者通过 Go install 安装:

go install github.com/nooga/let-go@latest

安装完成后,您可以使用 lg 启动 REPL,或者使用 lg myfile.lg 运行文件。对于高级工作流,lg -n 命令可以启动一个与 CIDER、Calva 和 Conjure 兼容的 nREPL 服务器,将 Clojure 强大的交互式开发体验带入 Go 生态系统。

Sources