← Back to Blogs
HN Story

let-go: Bringing Clojure's Expressiveness to Go's Runtime

May 11, 2026

let-go: Bringing Clojure's Expressiveness to Go's Runtime

For developers who love the expressiveness of Clojure but crave the deployment simplicity and startup speed of Go, the trade-off has traditionally been steep. Clojure's reliance on the JVM brings a significant "startup tax," making it challenging to use for CLI tools or serverless functions. While alternatives like Babashka have made strides in reducing this overhead, a new project called let-go aims to push the boundaries even further.

let-go is a bytecode compiler and VM written in Go that implements a language closely resembling Clojure. By leveraging Go's runtime and a custom bytecode format, it achieves a cold start of approximately 7ms and a tiny memory footprint, all while maintaining high compatibility with the Clojure ecosystem.

The Architecture of Speed

At its core, let-go is designed for minimal overhead. Unlike tree-walk interpreters, let-go utilizes a bytecode VM, which allows it to outperform other Go-based Lisp implementations. This architectural choice is evident in its benchmarks against other runtimes:

  • Startup Time: At 7ms, let-go is significantly faster than Babashka (20ms) and the JVM (331ms).
  • Binary Size: The resulting binary is roughly 10MB, making it 7x smaller than Babashka and 30x smaller than a standard JDK.
  • Memory Usage: Idle memory sits at 14MB, providing a lightweight profile suitable for constrained environments.

One of the most compelling features is the ability to compile programs into standalone executables. Using lg -b, developers can bundle their code into a single binary that requires no external runtime, effectively combining Clojure's syntax with Go's "single binary" distribution model.

Clojure Compatibility and Divergence

let-go is not intended to be a drop-in replacement for Clojure, but rather a highly compatible dialect. It has been tested against the clojure-test-suite, passing over 95% of assertions.

What's Included

The language provides a robust standard library, including clojure.core, clojure.string, and clojure.set. Notably, it implements clojure.core.async using real goroutines rather than an Inversion of Control (IOC) state machine, making the concurrency model feel native to the Go runtime.

Known Limitations

To achieve its performance and footprint, certain Clojure features are omitted or modified:

  • STM and Agents: Software Transactional Memory (Refs) and Agents are not implemented; users are encouraged to use atoms and channels.
  • Numeric Behavior: Integer overflow wraps silently on int64 rather than promoting to BigInt automatically (though explicit BigInt math is available via +', -', and *').
  • Regex: let-go uses Go's re2 syntax instead of Java's regex engine.
  • Sequences: Lazy sequences are unchunked, which simplifies the implementation but alters some performance characteristics.

Deep Integration with Go

Perhaps the strongest value proposition of let-go is its two-way interop with Go. It allows developers to embed a Clojure-like scripting layer directly into a Go application.

Struct and Record Mapping

Through RegisterStruct[T], Go structs can be mapped to let-go records. This allows for a zero-cost roundtrip for unmutated records, enabling Go developers to pass complex data structures into the VM and manipulate them using Clojure's powerful data-processing tools.

Channel Interop

Because let-go's concurrency is built on goroutines, Go channels (chan) can be plugged directly into let-go's go blocks and channel operations (<!, >!). This creates a seamless pipeline where events can be generated in Go and processed via a user-supplied Clojure script.

Expanding the Ecosystem: WASM and Pods

let-go extends beyond the desktop and server. It can compile programs into self-contained WASM web apps, including full terminal emulation via xterm.js. This allows Lisp-based tools to run entirely in the browser, with the added possibility of connecting an nREPL server via WebSockets to editors like Emacs (CIDER) or VS Code (Calva).

Furthermore, let-go supports Babashka pods. By implementing the binary protocol used by pods, let-go gains access to a wide array of existing extensions for databases, AWS, and Docker without needing to reimplement them from scratch.

Community Perspective

The emergence of let-go coincides with a broader trend of "compile-to-Go" projects. As one community member noted on Hacker News:

"There seems to be a surge in compile to Go projects recently. To me this signals that the runtime / stdlib of Go is one of the best out there... but the surface level (syntax) is too simple/verbose and lacks the expressiveness developers want."

While other projects like Joker, Janet, and Glojure offer similar paths toward a JVM-free Clojure experience, let-go distinguishes itself through its focus on bytecode performance, extreme startup speed, and deep Go runtime integration.

References

HN Stories