惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
N
Netflix TechBlog - Medium
V
Visual Studio Blog
博客园 - Franky
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
量子位
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
V
V2EX
The Cloudflare Blog
月光博客
月光博客
Last Week in AI
Last Week in AI
雷峰网
雷峰网
WordPress大学
WordPress大学
博客园 - 【当耐特】
博客园 - 聂微东
IT之家
IT之家
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

Hacker News: Best

madhadron - The seven programming ur-languages GitHub - smol-machines/smolvm: Tool to build & run portable, lightweight, self-contained virtual machines. I Measured Claude 4.7's New Tokenizer. Here's What It Costs You. Introducing Claude Design by Anthropic Labs It Is Time to Ban the Sale of Precise Geolocation The creative software industry has declared war on Adobe Isaac Asimov: The Last Question Newly unsealed records reveal Amazon’s price-fixing tactics, California attorney general claims Clojure - Documentary Android CLI and skills: Build Android apps 3x faster using any agent Qwen3.6-35B-A3B on my laptop drew me a better pelican than Claude Opus 4.7 Codex:全能型助手 Introducing Claude Opus 4.7 Qwen Studio The Future of Everything is Lies, I Guess: Where Do We Go From Here? Virginia Bans Sale of Geolocation Data YouTube now lets you turn off Shorts Burgers | マクドナルド公式 ChatGPT for Excel Ask HN: Who is using OpenClaw? Live Nation illegally monopolized ticketing market, jury finds Google Broke Its Promise to Me. Now ICE Has My Data. Open Source Isn't Dead. The Future of Everything is Lies, I Guess: New Jobs Unexpected €54k billing spike in 13 hours: Firebase browser key without API restrictions used for Gemini requests IPv6 – Google Your Backpack Got Worse On Purpose Good sleep, good learning, good life Fixing a 20-year-old bug in Enlightenment E16. Does Gas Town 'steal' usage from users' LLM credits & paid services to improve itself?
GitHub - nooga/let-go: Almost Clojure written in Go.
marcingas · 2026-05-10 · via Hacker News: Best

Squishy loafer

Tests

Greetings loafers! (λ-gophers haha, get it?)

This is a bytecode compiler and VM for a language closely resembling Clojure, a Clojure dialect, if you will. The smallest and fastest-starting Clojure-family language in Go — a single ~10MB binary with ~7ms cold start.

Why let-go?

  • Standalone executables — compile your program into a single binary with lg -b myapp main.lg. No runtime needed, just distribute and run.
  • WASM web apps — compile your program to a self-contained HTML page with lg -w outdir main.lg. Full terminal emulation via xterm.js, runs in any browser. Deploy to GitHub Pages or open locally.
  • Fast startup — 6ms cold start. Pre-compiled bytecode (LGB format) makes boot near-instant even with a large standard library.
  • Small footprint — 10MB binary, 14MB idle memory. 7x smaller than Babashka, 30x smaller than JDK.
  • Batteries included — core.async channels, HTTP server/client, JSON, Transit, IO, Babashka pods, nREPL server.
  • Go interop — embed let-go in Go apps, map Go structs to records, call Go functions from let-go and vice versa.
  • Broad Clojure compatibility — macros, destructuring, protocols, records, multimethods, transducers, lazy seqs, persistent data structures, BigInts.

Here are some nebulous goals in no particular order:

  • Quality entertainment,
  • Making it legal to write Clojure at your Go dayjob,
  • Implement as much of Clojure as possible — including persistent data types, true concurrency, transducers, core.async, and BigInts,
  • Provide comfy two-way interop for arbitrary functions and types,
  • AOT compilation — compile let-go programs to bytecode or standalone binaries,
  • Boot the entire runtime in a single requestAnimationFrame and still have 10ms to spare at 60fps,
  • Compile let-go programs to self-contained WASM web apps with terminal emulation,
  • nREPL server in WASM — connect Emacs/Calva to a let-go VM running in the browser via WebSocket,
  • Stretch goal: let-go bytecode -> Go translation.

Here are the non goals:

  • Being a drop-in replacement for clojure/clojure at any point,
  • Being a linter/formatter/tooling for Clojure in general.

Feature overview

let-go aims to feel like day-to-day Clojure, not to be a drop-in replacement. Most idiomatic code reads, runs, and behaves the same — but a non-trivial Clojure project will likely need some adjustments before it runs unmodified. See Known limitations below.

Clojure compatibility

let-go is tested against jank-lang/clojure-test-suite, a cross-dialect compliance suite:

4696 / 4921 assertions pass (95.4%) across 217 test files. The remaining gaps are mostly edge cases (overflow detection on +/-/*/inc/dec, BigInt promotion at the Long boundary, BigDecimal behavior) and a handful of stub namespaces — see below. Workflow guide: docs/clojure-test-suite.md.

Standard namespaces

Namespace Status
clojure.core Macros, destructuring, lazy seqs, transducers, protocols, records, multimethods, atoms, regex, metadata, BigInt
clojure.string Full
clojure.set Full
clojure.walk prewalk, postwalk, keywordize-keys, stringify-keys, walk
clojure.edn read, read-string
clojure.pprint pprint, cl-format
clojure.test deftest, is, testing, are, fixtures
clojure.core.async Channels, go/go-loop, alts!, mult/pub, pipe/merge/split (real goroutines, not IOC)
io Polymorphic readers/writers, slurp/spit, lazy line-seq, encoding, URLs, with-open
http Ring-style server + client, streaming responses
json read-json, write-json — float-preserving, record-aware
transit transit+json codec with rolling cache
os sh, stat, ls, cwd, getenv/setenv, exit, os-name, arch, user-name, hostname, separators
System JVM-shaped: getProperty, getProperties, getenv, exit, lineSeparator, currentTimeMillis, nanoTime. Exposes let-go.version, let-go.commit, user.home, user.dir, os.name, os.arch, etc.
syscall Direct Linux syscalls (mount, unshare, mknod, prctl, capset, seccomp, AppArmor) for systems programming
pods Babashka pods over JSON / EDN / transit

Babashka pods

let-go supports Babashka pods - standalone programs that expose namespaces over a binary protocol. This gives let-go access to the entire pod ecosystem: databases, AWS, Docker, file watching, and more.

;; Load a pod (uses babashka's shared cache)
(pods/load-pod 'org.babashka/go-sqlite3 "0.3.13")

;; Use it like any other namespace
(pod.babashka.go-sqlite3/execute! "app.db"
  ["create table users (id integer primary key, name text)"])
(pod.babashka.go-sqlite3/execute! "app.db"
  ["insert into users values (1, ?)" "Alice"])
(pod.babashka.go-sqlite3/query "app.db"
  ["select * from users"])
;; => [{:id 1 :name "Alice"}]
  • pods/load-pod - load by name (PATH) or from babashka cache (symbol + version)
  • Supports JSON, EDN, and transit+json payload formats
  • Client-side code evaluation (pod-defined macros and wrappers)
  • Async streaming via pods/invoke with :handlers for callbacks
  • Shares ~/.babashka/pods/ cache - install pods with bb, use them from lg

See the pod registry for available pods. Install pods with babashka:

bb -e '(pods/load-pod (quote org.babashka/go-sqlite3) "0.3.13")'

Go interop

  • RegisterStruct[T] — map Go structs to let-go records with cached field converters
  • ToRecord[T] / ToStruct[T] — zero-cost roundtrip for unmutated records
  • BoxValue auto-converts registered structs to records
  • Boxed Go values expose methods via .method interop syntax
  • .field access on records

Benchmarks

Benchmarks compare let-go against Babashka (GraalVM native), Joker (Go tree-walk interpreter), and Clojure on the JVM. Each benchmark is valid Clojure that runs unmodified on all runtimes. Run benchmark/run.sh to reproduce (requires hyperfine, bb, clj, joker).

let-go babashka joker clojure JVM
Platform Go bytecode VM GraalVM native Go tree-walk interpreter JVM (HotSpot)
Binary size 10M 68M 26M 304M (JDK)
Startup 7ms 20ms 12ms 331ms
Idle memory 14MB 27MB 21MB 92MB

Performance highlights (Apple M1 Pro):

  • Smallest footprint — 7x smaller than Babashka, 30x smaller than the JDK
  • Fastest startup — 7ms with pre-compiled bytecode (fits in a requestAnimationFrame), 3x faster than Babashka, 2x faster than Joker, 48x faster than JVM
  • Wins on short-lived tasks — map/filter and transducer pipelines: 8ms vs bb's 19ms (2.4x faster)
  • Competitive on compute — fib(35) within 4% of Babashka (1.98s vs 1.90s), loop-recur 1.8x faster
  • Lowest memory — 14MB for fib(35) vs bb's 77MB (5.4x less), 20MB for reduce 1M vs bb's 59MB (3x less)
  • 10x+ faster than Joker on most compute benchmarks — bytecode VM vs tree-walk interpreter

Full results with methodology: benchmark/results.md

Known limitations and divergence from Clojure

Not implemented

  • Refs / STM — atoms + channels cover practical concurrency needs
  • Agents — use go blocks and channels instead
  • Hierarchies (derive, underive, ancestors, descendants, parents) — stub only; multimethod dispatch works, but isa? chains do not
  • with-precisionBigDecimal itself works (M literals, bigdec, decimal?, exact arithmetic), but with-precision is a no-op so explicit rounding control is missing
  • Chunked sequences — lazy seqs are unchunked (simpler, slightly different perf characteristics)
  • Reader tagged literals (#inst, #uuid)
  • deftype — use defrecord instead
  • reify — protocols can only be extended to named types
  • Spec — no clojure.spec
  • alter-var-root — vars are mutable but no alter-var-root
  • Numeric overflow detection+/-/*/inc/dec wrap silently on int64 overflow rather than promoting to BigInt; use +'/-'/*' for explicit BigInt math
  • subseq / rsubseq — sorted collections themselves work (sorted-map, sorted-set, sorted-map-by, sorted-set-by, rseq), but range queries on them are not yet implemented

Known behavioral differences

  • concat* (used internally by quasiquote) is eager — the user-facing concat is lazy, matching Clojure
  • All channel operations block<! and <!! are identical (Go channels are always blocking), same for >!/>!!
  • go blocks are real goroutines — no IOC (inversion of control) state machine like Clojure's core.async; this means they're cheaper but go blocks can call blocking ops directly
  • No BigDecimal — numeric tower is int64 + float64 + BigInt (no arbitrary-precision decimals)
  • Regex is Go flavorre2 syntax, not Java regex
  • letfn uses atoms internally for forward references — slight overhead vs Clojure's direct binding

Examples

Real projects written in let-go:

  • xsofy — a roguelike that runs in the browser and the terminal from the same source
  • lgcr — a decent daemonless container runtime, built on the syscall namespace

In this repo:

  • examples/ — small programs
  • test/.lg test files covering all features

Try online

Check out this bare-bones online REPL. It runs a WASM build of let-go in your browser!

Installation

Homebrew (macOS / Linux)

brew tap nooga/let-go https://github.com/nooga/let-go
brew install let-go

Download binary

Grab a prebuilt binary from Releases — available for Linux, macOS, and Windows on amd64/arm64.

From source

Requires Go 1.22+.

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

Usage

lg                                 # REPL
lg -e '(+ 1 1)'                   # eval expression
lg myfile.lg                       # run file
lg -r myfile.lg                    # run file, then REPL
lg -w outdir myfile.lg             # compile to WASM web app

Compilation and distribution

let-go can compile programs to bytecode (.lgb files) and package them as standalone executables.

Compile to bytecode — skips the reader/parser/compiler at load time:

lg -c app.lgb app.lg               # compile to bytecode
lg app.lgb                          # run bytecode directly

Create a standalone binary — bundles the compiled bytecode into a self-contained executable:

lg -b myapp app.lg                  # compile + bundle into executable
./myapp                             # runs anywhere, no lg needed

The standalone binary is a copy of lg with your program's bytecode appended. It needs no external files or runtime — just copy it to another machine and run it.

Build a WASM web app — compiles your program into a single HTML page that runs in the browser:

lg -w site app.lg                   # compile to web app
open site/index.html                # open in browser

The output directory contains:

  • index.html — self-contained (~6MB, inlined WASM + wasm_exec.js, gzip-compressed)
  • coi-serviceworker.js — enables cross-origin isolation for interactive apps (needed on GitHub Pages)

Programs using the term namespace get full terminal emulation via xterm.js — ANSI colors, cursor positioning, raw keyboard input all work. The Go WASM runtime runs in a Web Worker with SharedArrayBuffer for blocking term/read-key.

For GitHub Pages deployment, just point Pages at the output directory. The service worker handles the required COOP/COEP headers automatically.

Detecting AOT compilation — the *compiling-aot* var is true during -c, -b, and -w compilation, false at runtime. Use it to prevent side effects (like starting a server or game loop) from running at compile time:

(defn -main []
  (start-server))

(when-not *compiling-aot*
  (-main))

Detecting WASM at runtime — the *in-wasm* var is true when running inside a WASM web app, false in native mode. Use it to disable file I/O, adjust animation timing, or enable browser-specific behavior:

(when-not *in-wasm*
  (spit "debug.log" "only in native mode"))

Building from source

go run .                           # run from source
go build -ldflags="-s -w" -o lg .  # ~9MB stripped binary

nREPL

let-go includes an nREPL server compatible with CIDER (Emacs), Calva (VS Code), and Conjure (Neovim).

lg -n                              # start nREPL on default port (2137)
lg -n -p 7888                      # start nREPL on port 7888

The server writes .nrepl-port in the current directory so editors auto-discover it.

Supported ops: clone, close, eval, load-file, describe, completions, complete, info, lookup, ls-sessions, interrupt

Emacs (CIDER): M-x cider-connect-clj, host localhost, port from .nrepl-port

VS Code (Calva): Open a let-go project — the included .vscode/settings.json registers a custom connect sequence. Use "Calva: Start a Project REPL and Connect (Jack-In)" and pick "let-go", or "Calva: Connect to a Running REPL Server" if the nREPL is already running.

Neovim (Conjure): Should auto-connect when .nrepl-port exists

Embedding in Go

let-go embeds cleanly as a scripting layer for Go programs — define Go values and functions, hand them to the VM, and run user-supplied Clojure against your data. Go structs roundtrip as records, Go channels are first-class let-go channels, and Go functions are callable from let-go code.

import (
    "github.com/nooga/let-go/pkg/api"
    "github.com/nooga/let-go/pkg/vm"
)

c, _ := api.NewLetGo("myapp")

// Expose Go values and functions to let-go
c.Def("x", 42)
c.Def("greet", func(name string) string {
    return "Hello, " + name
})

v, _ := c.Run(`(greet "world")`)
fmt.Println(v) // "Hello, world"

Struct ↔ Record roundtrip. Registered structs become records on the let-go side. Unmutated values unbox back to the original Go type for free; mutated ones go through vm.ToStruct[T].

type Item struct{ Name string; Price float64; Qty int }
vm.RegisterStruct[Item]("myapp/Item")

c.Def("item", Item{Name: "Widget", Price: 9.99, Qty: 5})
c.Run(`(:name item)`)        // "Widget"
c.Run(`(* (:price item) (:qty item))`) // 49.95

// Define a let-go function that processes Go structs
c.Run(`(defn total [it] (* (:price it) (:qty it)))`)
v, _ := c.Run(`(total item)`) // 49.95

Streaming via Go channels. A Go chan int and a vm.Chan plug straight into go/<!/>! — perfect for piping events through a user-supplied script.

inch := make(chan int)
outch := make(vm.Chan)
c.Def("in", inch)
c.Def("out", outch)

c.Run(`(go (loop [i (<! in)]
             (when i
               (>! out (inc i))
               (recur (<! in)))))`)

See pkg/api/interop_test.go for the full set of embedding examples (defs, structs, channels, function calls).

Testing

go test ./... -count=1 -timeout 30s

🤓 Follow me on X 🐬 Check out monk.io