案例 Go 版 Clojure 来了!let-go 让你在 Golang 里写 Lisp

2026-05-11 20:51:22 +0800 CST views 5

Go 版 Clojure 来了!let-go 让你在 Golang 里写 Lisp

标签: let-go / Clojure / Go / Lisp / 函数式编程 / 编译器 / 虚拟机
原文: 微信公众号「编程悟道」https://mp.weixin.qq.com/s/fbQMxAMu1xQzr1euyJvqmg


核心亮点

这个项目的作者在 2021 年干了一件特别"骚"的事:他用 Go 写了一个 Clojure 方言的编译器,还带了个栈虚拟机。

  • 10MB 二进制文件,冷启动只要 7 毫秒,连 JVM 都不需要
  • 通过了 jank-lang 测试套件 95.4% 的测试(4696/4921 个断言)
  • Go + Clojure 双向互操作:在 10MB 的二进制里同时享受 Clojure 的函数式编程快感和 Go 的生态
  • 支持 WASM:可以编译成自包含的网页,带终端模拟
  • 甚至能在 Plan 9 上运行(你没看错,就是那个古老的 Plan 9 操作系统)
  • 项目名"let-go":既是"let go"(放手)的谐音,也是"let"+"Go"的组合

一、问题:想写 Clojure,但讨厌 JVM?

Clojure 是一门非常优雅的 Lisp 方言,以其不可变数据结构、持久化数据结构、惰性序列、变换器(transducers)等特性著称。

但 Clojure 跑在 JVM 上,这就带来了问题:

  • JVM 启动太慢:冷启动 363ms,内存占用 92MB
  • 体积太大:JDK 本身就要 304MB
  • 不是所有场景都适合:写个脚本还要装 JDK?

于是,有人开始做轻量级的 Clojure 实现:

实现体积启动时间内存占用
Babashka68MB18ms27MB
Joker26MB12ms22MB
Clojure JVM304MB(JDK)363ms92MB
let-go~10MB7ms13.5MB

let-go 在"小而快"这个维度上,简直是降维打击


二、let-go 是什么?

let-go 是一个 Clojure 方言,它有一个字节码编译器和栈虚拟机,全部用 Go 实现。

GitHub:https://github.com/nooga/let-go
作者:nooga
协议:MIT
主要语言:Go

作者在 README 里说得很坦诚:

"我 2021 年开始做这个,本来就是个精心设计的玩笑——一个借口,让我在写 Go 的时候也能写 Clojure。"

结果这个"玩笑"越做越大,现在变成了一个真正有用的工具。作者用它来写 CLI 工具、脚本、Web 服务器,甚至基于它构建了一个无守护进程的容器运行时(lgcr)

你没看错,有人用这个"玩笑项目"搞了个容器运行时。


三、它能做什么?

let-go 支持的功能列表,看得我头皮发麻:

核心特性

  • 持久化数据结构:Clojure 那套不可变数据结构的精髓
  • 惰性序列:lazy seq,你懂的
  • 变换器:transducers,函数式编程的瑞士军刀
  • 协议和记录:protocols 和 records,Clojure 的面向对象
  • 多重方法:multimethods,比 Java 的重载优雅一万倍
  • core.async:Go 的 goroutine 风格并发,但用 Clojure 的语法写
  • 大整数:BigInts,数学计算不怕溢出

Go 双向互操作

更重要的是,它能和 Go 双向互操作。你可以:

  • 在 let-go 里调用 Go 的函数
  • 操作 Go 的结构体
  • 使用 Go 的 channel

这意味着什么?意味着你可以在一个 10MB 的二进制文件里,同时享受 Clojure 的函数式编程快感和 Go 的生态。


四、性能对比

来看一组真实的基准测试数据(Apple M1 Pro):

测试项let-goBabashkaJokerClojure JVM
map/filter7.9ms21.5ms82ms15ms
持久化 map 操作20.8ms23.7ms95ms12ms
fib(35)2.08s2.15s8.9s0.8s
tak(18,12,6)1.2s1.3s5.1s0.5s

结论

  • 在短生命周期的数据操作上,let-go 表现最好
  • 在长时间运行的计算密集型任务上,JVM 的 HotSpot JIT 编译后能反超
  • let-go 比 Babashka 快约 3 倍,比 Joker 快 10 倍以上(字节码 VM vs 树遍历解释器)

五、代码示例

看看它到底有多像 Clojure:

;; 定义一个函数
(defn greet [name]
  (str "Hello, " name "!"))

;; 使用持久化数据结构
(def my-map {:name "Alice" :age 30 :city "Tokyo"})

;; 惰性序列
(def fibs
  (lazy-cat [0 1] (map + fibs (rest fibs))))

;; 变换器
(def xf (comp (filter even?) (map inc)))
(transduce xf + (range 10))

;; core.async
(require '[clojure.core.async :as async])
(def ch (async/chan 10))
(async/go
  (async/>! ch "hello"))
(async/<!! ch)

看到没?这就是 Clojure,纯正的 Clojure。不需要改任何语法,直接跑。


六、支持哪些标准库?

let-go 实现了 Clojure 的大部分标准库:

Clojure 标准库

  • clojure.core:宏、解构、惰性序列、变换器、协议、记录、多重方法、原子、正则、元数据、大整数
  • clojure.string:完整实现
  • clojure.set:完整实现
  • clojure.walk:prewalk、postwalk 等
  • clojure.edn:read、read-string
  • clojure.pprint:pprint、cl-format
  • clojure.test:deftest、is、testing 等
  • clojure.core.async:channel、go/go-loop、alts!等(用真正的 goroutine 实现)

自定义命名空间

  • io:多态读写器、slurp/spit、惰性行序列、编码、URL、with-open
  • http:Ring 风格的服务器+客户端、流式响应
  • json:read-json、write-json(浮点数保留、记录感知)
  • transit:transit+json 编解码器
  • os:sh、stat、ls、cwd、getenv/setenv、exit 等
  • System:JVM 风格的 getProperty、getenv、exit 等
  • syscall:直接 Linux 系统调用(mount、unshare、mknod、prctl 等)

七、编译目标

let-go 的编译目标包括:

  • 独立二进制文件:编译成可执行文件,直接运行
  • WASM 网页:编译成自包含的 WASM 网页,带终端模拟
  • Plan 9:甚至能在 Plan 9 上运行(你没看错,就是那个古老的 Plan 9 操作系统)

作者还计划实现:

  • 浏览器中的 nREPL(let-go VM 跑在 WASM 里,编辑器通过 WebSocket 连接)
  • let-go 字节码到 Go 的翻译

八、快速上手

# 安装 let-go
go install github.com/nooga/let-go@latest

# 运行一个 REPL
let-go

# 编译一个脚本到独立二进制
let-go compile my-script.lg -o my-script

# 编译到 WASM 网页
let-go compile my-script.lg --wasm -o my-script.html

九、为什么你应该关注这个项目?

  1. 如果你喜欢 Clojure 但讨厌 JVM:let-go 给了你一个轻量级的替代方案
  2. 如果你写 Go 但想试试函数式编程:let-go 让你在 Go 的生态里写 Clojure
  3. 如果你需要快速启动的脚本语言:7ms 冷启动,比 Python 都快
  4. 如果你对语言实现感兴趣:这个项目的代码结构清晰,是学习编译器实现的好材料

十、一些有趣的细节

  • 项目名"let-go":既是"let go"(放手)的谐音,也是"let"+"Go"的组合
  • 作者自称"loafers":λ-gophers 的谐音,λ 代表 lambda 演算,gophers 是 Go 社区的昵称
  • 项目 Logo:一只穿着拖鞋的可爱生物,作者叫它"Squishy loafer"

十一、已知的限制

let-go 不是 JVM Clojure 的即插即用替代品。它不能加载 JAR 文件,也不打算支持。大部分惯用的 Clojure 代码可以不加修改地运行,但一个依赖真实库的项目可能需要调整。

具体来说:

  • 不支持 Java 互操作(废话,没有 JVM)
  • 不支持 AOT 编译到 JVM 字节码
  • 一些数值边界情况(溢出检测、BigInt 提升等)可能不同

写在最后

let-go 这个项目,让我想起了那句话:"最好的玩笑,往往是最认真的。"

作者 nooga 用一个"玩笑"开始的项目,现在变成了一个功能完整、性能出色、实用性强的 Clojure 方言实现。它证明了:有时候,最好的创新来自于"不务正业"的探索

如果你对函数式编程感兴趣,或者想在你的 Go 项目里引入一些 Clojure 的优雅,let-go 绝对值得一试。

毕竟,10MB 的二进制、7ms 的冷启动、95.4% 的测试通过率——这已经不是一个玩具了。


本文整理自微信公众号「编程悟道」,原文链接:https://mp.weixin.qq.com/s/fbQMxAMu1xQzr1euyJvqmg

推荐文章

Vue3中的响应式原理是什么?
2024-11-19 09:43:12 +0800 CST
记录一次服务器的优化对比
2024-11-19 09:18:23 +0800 CST
Go 协程上下文切换的代价
2024-11-19 09:32:28 +0800 CST
如何在 Vue 3 中使用 Vuex 4?
2024-11-17 04:57:52 +0800 CST
Shell 里给变量赋值为多行文本
2024-11-18 20:25:45 +0800 CST
全新 Nginx 在线管理平台
2024-11-19 04:18:33 +0800 CST
程序员茄子在线接单