Clojure,
hosted on Go.
A Go-written compiler that AOT-emits plain Go source (the ClojureScript model, Go as the JavaScript), plus a tree-walk evaluator that is the REPL. Same code runs at the prompt and compiles to a static binary — byte-identical output on both paths.
$ cljgo repl cljgo 0.0.1-m2 user=> (require-go '[strings]) user=> (strings/ToUpper "clojure on go") ;; a real Go call, zero bindings "CLOJURE ON GO" user=> (defn fact [n] (if (< n 2) 1 (* n (fact (- n 1))))) user=> (fact 20) 2432902008176640000
Five priorities, in order
Not a wishlist — the design contract. Everything in cljgo is judged against these, Clojure-first.
1 · Universal interop
Any Go module is importable and callable with zero hand-written bindings — the Go ecosystem is the standard library. C reaches in via cgo modules and purego FFI.
2 · REPL-driven
The tree-walk evaluator is a real Clojure REPL: live re-def, defmacro at the prompt, namespaces, eval, resolve.
3 · Faithful Clojure
Persistent data structures with real structural sharing, transients, a numeric tower, macros as plain fns, seqs, and vars as the indirection layer.
4 · Fast in both modes
Fast tree-walk and a compiled performance ladder — a feature, not an option. Benchmarked in CI; a perf regression is treated like a conformance failure.
5 · cgo is first-class
CGO_ENABLED=1 projects are supported, not tolerated — cgo-based Go modules (sqlite drivers, sensors, GUI/audio) import like anything else.
Dual-mode is the gate
Interpreted result == compiled result. A dual-harness conformance suite enforces it on every commit; a REPL↔binary divergence is a release blocker.
Zero-binding Go interop
require-go pulls in any Go package and calls it directly — no wrappers, no generated stubs. The Go toolchain is the classpath. This runs identically interpreted and compiled.
(require-go '[strings]) (require-go '[strconv]) (require-go '[math]) ;; single-return package fns + int64→int arg coercion (println "ToUpper:" (strings/ToUpper "hello")) (println "Repeat:" (strings/Repeat "ab" 3)) ;; a const in value position (println "Pi:" math/Pi) ;; (T, error) call → [v err] vector; happy-path err slot is nil (println "Atoi:" (strconv/Atoi "123")) ;; the ! suffix unwraps-or-throws — returns the widened value (println "Atoi!:" (strconv/Atoi! "456"))
Errors as values
(T, error) Go calls shape to a [v err] vector — branch on the error slot, Clojure-style. The ! suffix unwraps or throws.
Members, ctors & real goroutines
(.Method r …), (.-Field r), (pkg/T. {…}) constructors, plus (chan) / (>! c v) / (<! c) / (go …) over real goroutines — no CPS rewrite.
build.cljgo declares (go-require app "github.com/gorilla/websocket" "v1.5.3") — cljgo synthesizes the go.mod, resolves signatures from go/packages type facts, and the emitted binary makes the real call. A websocket client, zero hand-written bindings.
Dependencies, and a citizen of both ecosystems
Declare a dependency as code in build.cljgo — no deps.edn. Then publish
the same pure-Clojure library to Go developers (a go-gettable module) and JVM-Clojure
developers (source), from one build description.
1 · Depend on a library
(defn build [b] (dep b "greetlib" {:path "../greetlib"}) ;; or a pinned git coordinate: ;; (dep b "greetlib" {:git "https://…" :ref "v1.2.0"}) (let [app (exe b {:name "app" :main "src/app/core.cljg"})] (install b app) (run b app)))
2 · Build resolves it, both legs
$ cljgo build run Hello, world, from greetlib! ;; a committed build.lock.edn pins it: {:deps [{:name "greetlib" :paths ["src"] :pure? true :requires []}] :lock/version 1} $ cljgo run src/app/core.cljg # same dep, interpreter Hello, world, from greetlib! # byte-identical
3 · Publish to Go
$ cljgo publish go wrote go-gettable module example.com/greet (ns greet.core, 2 exported) ;; a Go dev then just: go get example.com/greet ;; pkg.Hello("world"), pkg.Shout("world")
4 · Publish to Clojars
$ cljgo publish clojars wrote pure Clojure source tree + deps.edn $ cljgo publish clojars # if it uses Go interop: error: cannot publish to the JVM — greet.core (core.cljg:3) require-go call: uses Go interop
Purity is the gate, checked at publish
A pure-Clojure library reaches both worlds. The moment a namespace uses require-go/ffi it is
Go-side only — and publish clojars refuses it with the offending file:line, not a broken download.
Reproducible by construction
A global content-addressed cache (keyed by identity, verified by a merkle tree hash — a git SHA is not a content
hash), a committed build.lock.edn, and a hard error on a version conflict. One resolver feeds both
legs, so a dependency resolves identically interpreted and compiled.
One analyzer, two backends
The unforgivable failure mode is the REPL diverging from the compiled binary. cljgo makes that structurally hard: one reader, one analyzer, one AST — feeding both a tree-walk evaluator and a Go-source emitter.
def and defmacro at the prompt.go/packages type facts — direct, non-reflective calls. The Go toolchain then produces a static native binary.Compatibility you can measure
cljgo is scored against the jank clojure-test-suite — 242 real
clojure.core files — as a single ratcheting number in CI. Not a self-graded checklist.
- 100% of tested
clojure.corevars resolve (242 / 242). - 98.3% of files pass — 0 failures, 4 errors. Reproduce it yourself:
cljgo suite. - Coverage ratchet — the passing count may only rise.
- Runs interpreted; cljgo's own dual-mode conformance stays fully green alongside it.
:default, so a runtime
the suite has never heard of misreads them. With :cljgo branches
added — the same mechanism :cljr/:lpy/:phel
use — it reads 242/242, but that is not upstreamed, so we publish 98.3%.
Where we actually stand
Every row below is reproducible with the command next to it. Apple M5 Pro, go1.26.3,
hello.clj = (println "hi"). We publish the ones we lose on too.
Full tables — let-go, babashka, joker, JVM Clojure, Glojure, and the web-framework
shootout — live on the benchmarks page.
| Metric | cljgo | Reproduce |
|---|---|---|
| Tool binary | 27.5 MB stripped | go build -trimpath -ldflags="-s -w" ./cmd/cljgo |
| Compiled binary, hello | 6.7 MB | cljgo build hello.clj |
| Compiled startup, hello | 5.0 ms (was 28.9 ms pre-AOT-core) | hyperfine -N ./hello |
| Peak RSS, hello | 11.5 MB | /usr/bin/time -l ./hello |
| Interpreter boot | 31.7 ms (a larger core loads now) | go test -bench=BenchmarkBoot -benchmem -run '^$' ./pkg/eval/ |
| Emitted vs handwritten Go | ~5× (target ~10×, reached and passed 2026-07-23) | go test -run TestFactorialPerfBudget ./pkg/emit/ |
| clojure-test-suite | 238 / 242 (98.3%) | cljgo suite |
Where it wins: the compiled leg takes every recursion and data-structure row —
tak/fib at 34.6 / 24.7 ms, 13× and 17× faster than JVM Clojure — and ships
the smallest binary of the three Clojure-on-Go compilers (6.7 MB vs Glojure's 7.5 and let-go's 12.8).
Startup is a dead heat with let-go at 5.0 ms. On (reduce + (range 1000)) it peaks at
13 MB RSS against JVM Clojure's 102 MB — ~7× less memory, measured, and that's the
JVM's best case: a hello that exits.
Where it loses:
transducers and reduce still go to babashka's
purpose-built core (16.4 vs 13.0 ms, 26.0 vs 20.0 ms). And in the AOT-vs-AOT table
Glojure wins 6 of 8 rows — its codegen ships reduce-pipeline fusion and float64 specialization we
haven't built yet. The interpreted leg (cljgo run) is a tree-walker and loses to everything
except joker; that's the dev path, not what you deploy.
Every table, every version, both losses in full → benchmarks page.
bash benchmark/run.sh.
Quickstart
Install once, then: a REPL, run a Clojure file, or build a whole build.cljgo project.
Needs a Go 1.26 toolchain (and a C toolchain with CGO_ENABLED=1 for cgo interop features).
1 · Install cljgo
# with a Go 1.26+ toolchain
go install github.com/muthuishere/cljgo/cmd/cljgo@latest
cljgo run and Go interop straight away — those work from the
binary alone, with no Go toolchain on PATH. Compiling to a native binary
(cljgo build) does need the toolchain: it emits Go source and invokes
go build. The binary pins the published runtime module in the generated
go.mod, so the first build fetches it from the module proxy once per machine
(~1 MB, a few seconds) — no cljgo checkout required.
Prefer a prebuilt binary? Grab one from the
latest release.
2 · Start a REPL
$ cljgo repl cljgo 0.1.0-dev user=> (defn square [x] (* x x)) #'user/square user=> (map square [1 2 3 4]) (1 4 9 16) user=> (require-go '[strings]) ;; call Go, live user=> (strings/ToUpper "hi") "HI"
3 · A Clojure file — hello.clj
;; hello.clj (defn fact [n] (if (< n 2) 1 (* n (fact (- n 1))))) (println "hello from cljgo") (println "(fact 10) =" (fact 10))
4 · Run it, then compile it
$ cljgo run hello.clj hello from cljgo (fact 10) = 3628800 $ cljgo build hello.clj # → ./hello $ ./hello hello from cljgo (fact 10) = 3628800 # byte-identical
5 · Or a whole project — build.cljgo
;; build.cljgo — the build is a program (defn build [b] (let [app (exe b {:name "app" :main "src/app/core.cljg"})] (install b app) (run b app)))
Build & run the project
$ cljgo build # → installs ./app $ cljgo build run # build it, then run it hello from the project # declare deps + third-party Go right in build.cljgo: # (dep b "greetlib" {:path "../greetlib"}) # (go-require app "github.com/gorilla/websocket" "v1.5.3")
CLI surface
cljgo repl # start a REPL cljgo nrepl # nREPL server — Calva / CIDER connect cljgo run <file.clj> # evaluate a file (interpreted) cljgo build [-o out] <file.clj> # compile a file to a native binary cljgo build # project mode: run ./build.cljgo cljgo new <name> # generate a project (lib | cli | web) cljgo test # run the project's clojure.test tests cljgo publish <go|clojars> # publish the library to Go or Clojars cljgo suite # run the jank clojure-test-suite cljgo cache clean # clear the global dependency cache cljgo check <file.clj> [--json] # analyze, report diagnostics cljgo explain <code> [--json] # show an error code's explain page cljgo version # print the version string
Status & roadmap
A working REPL and a native compiler. Milestones landed to date:
| Milestone | State | What landed |
|---|---|---|
| M0–M1 | ✅ | REPL: full syntax-quote reader, loop*/recur, dynamic vars, namespaces, macroexpansion, defmacro at the prompt, embedded core.clj, clojure.test |
| M2 | ✅ | cljgo build → native binary, <10 ms startup, fixed-arity calling convention |
| M3 | ✅ | Zero-ceremony Go interop, both modes — require-go, package fns/consts, members (.Method r …)/(.-Field r), ctors, (T,error)→[v err], ! unwrap |
| core.async | ✅ | Real clojure.core.async over goroutines — no CPS rewrite. 55 publics = every non-deprecated, non-internal var of JVM core.async 1.6.681: alts!/alt!, timeout, transducers, mult/pub/mix/pipe, pipeline(-blocking/-async) |
| AOT-core | ✅ | The startup lever, shipped: clojure.core AOT-compiled into the binary instead of evaluated at boot — compiled startup 28.9 → 6.5 ms at the time (today's number is 5.0 ms, via the 2026-07-23 campaign below) |
| perf 07-23 | ✅ | The 2026-07-23 campaign (ADRs 0063–0067): chunk-aware seq ops, the IFn2 2-arg reduce seam, direct-call emission, the sealed-core dirty flag, int64 numeric inference, <=/>= unboxed compares, startup clawback — emitted-vs-handwritten Go ~35× → ~5×, fib 975 → 24.7 ms, startup back to 5.0 ms, CI-gated |
| deps | ✅ | Dependency resolution — (dep …) in build.cljgo, content-addressed cache, committed build.lock.edn, hard error on version conflict, one resolver both legs |
| publish | ✅ | cljgo publish go (go-gettable module) + publish clojars (pure Clojure source), purity-gated at file:line — one library, both ecosystems |
| build.cljgo | ✅ | Zig-style build graph — exe/install/run + go-require third-party Go (gorilla/websocket, zero bindings) |
| bri T0–T1 | ✅ | The app framework — cljgo new/cljgo dev (server + nREPL), HTTP + hiccup HTML + routes/middleware, signed-cookie sessions + CSRF, layered config (cljgo config) |
| core batches | ✅ | Numeric tower (bigint/bigdec/ratios/promotion/bit-*), transients, JVM-compatible hashing, reify, tagged-literals/reader-conditionals, richer error rendering, suite ratchet |
| Next | ◦ | ADR 0067 follow-ups (float64, multi-arity/variadic specialization, capturing-closure lift); reduce/transducers vs babashka's core (the two rows still lost); the batteries (see below): bri data layer + jobs/cache, a curated Go-native stdlib, layered config + vault + i18n; plus C FFI (purego) and comptime |
Where it's headed — the Bun of Clojure
The direction: one fast native binary, batteries included, zero-config — Bun's ergonomics with Go's delivery and no runtime to distribute. Some of this has shipped, some is still ahead; the batteries stay native-fast and keep the single static binary:
- Data —
cljg.data.cast: a pure-Go SQLite as the zero-install default DB, Postgres (pgx) for production, mass-assignment-safe casts, and migrations (cljgo migrate up/status/new, auto-applied in dev). (bri T2, shipped) - Jobs & cache — shipped as pure zero-dependency fundamentals: an in-process core.async job queue
(
cljg.jobs) and a TTL/singleflight cache (cljg.cache), each behind a protocol you canreify. A durable Postgres queue or Redis cache stays a you-bring-it backend, not a bundled dependency. (bri T3, shipped) - A curated Go-native stdlib — secrets (OS keychain), streaming file I/O, crypto/hashing, http-client,
websocket — there by default, no
require-goceremony. (partly shipped) - Spring-Boot-style config — one layered chain (defaults →
application.edn/.properties→ profiles →APP_*env → pluggable vault → overrides), plus i18n message bundles on the same infra.
The Zig model
cljgo's "batteries" are Zig's, not Leiningen's / deps.edn's (design/08).
- build.cljgo — the build is a program, not a data file.
(defn build [b] …)defines an artifact/step DAG;cljgo build/build run/testmirrorzig build. Replaces deps.edn for the AOT product. - Dependencies as code —
(dep …)lives inbuild.cljgo, not a separate manifest. Resolution is content-addressed and lockfile-pinned, one resolver feeds both legs, and a Java-carrying dependency fails loud rather than at link time. Nodeps.ednin either direction. - Publish both ways — a pure-Clojure library ships to Go developers (
cljgo publish go, a go-gettable module) and JVM-Clojure developers (cljgo publish clojars, source), from one build description, gated on purity at publish time. - comptime — Zig-style compile-time value execution alongside Clojure macros. Macros transform syntax;
comptimecomputes values embedded as literals. In the REPL, compile-time == eval-time. - Cross-compilation — cljgo emits plain Go, so pure-Go + purego programs build for any OS/arch with no target toolchain.
--target os/arch, or a matrix per artifact.