Skip to content

FAQ

Questions people have actually asked — on Slack, Reddit, and elsewhere — answered honestly.

How is this different from Glojure and let-go?

Less than an earlier version of this entry claimed — that version was written from the projects’ public docs, and their docs undersold them. This one comes from reading their source. All three projects compile Clojure to Go source and then to native binaries: Glojure has a real per-namespace Go generator (int64/float64 specialization, direct calls, reduce-pipeline fusion), and let-go lowers its IR to real go/ast (via gloat, the automation tool for both). “Three attempts at the same thing” is fair framing.

The deltas we believe are real. Where compilation starts: cljgo is the ClojureScript model — one analyzer, one AST, and the emitter compiles source forms directly, so nothing has to be evaluated to be compiled; Glojure’s generator walks an already-evaluated namespace; let-go lowers its bytecode IR. What ships: cljgo’s compiled binary links zero interpreter (CI checks the link set); Glojure’s compact AOT builds retain the evaluator; let-go’s lowered binaries keep the VM runtime linked. The rest is product surface — conformance frozen against JVM Clojure 1.12.5 through both REPL and binary, the bri batteries, the toolchain — positioning, not architecture.

And measured head-to-head, AOT vs AOT vs AOT on the same programs (2026-08-02), it is two clear wins each and four ties: cljgo takes tak and fib and ships far smaller binaries; Glojure takes transducers and, by 3.7×, reduce; the other four rows sit inside the run-to-run noise. The table — including the reduce loss and how it was checked — is published as-is on the benchmarks page.

Is cljgo just a fork of Glojure?

No — but part of it is a fork, on purpose, and it’s credited. pkg/lang (the persistent data structures and runtime types) is a hard fork of Glojure’s pkg/lang at v0.6.8: 23.4k lines of Go, EPL license preserved, a full PROVENANCE.md with the surgery log. Persistent vectors and maps don’t need reinventing; that’s the layer where sharing is a feature, not a secret.

Everything above it is original: ~85k lines of Go (reader, analyzer, tree-walk evaluator, Go-source emitter, Go-native corelib, bri, diagnostics, the new/dev/build/dist toolchain), ~8k lines of Clojure-in-Clojure core, and ~490 conformance tests frozen against real JVM Clojure 1.12.5, each run through both the interpreter and the compiled binary. So “same codebase” is true for the data-structure layer — about 22% of the Go — and false for the compiler, which is the part that makes cljgo cljgo. Shared runtime foundation, credited; independent compiler and product on top.

What is cljgo actually for, day to day?

Web APIs and CLIs, shipped as one small static binary — the kind of thing you run on a cheap VPS or in a minimal container without a JVM, a runtime install, or a fat base image. Hello-world is a 7.1 MB static binary with ~5 ms startup; cljgo dist cross-compiles it for every OS/arch in one command.

The batteries follow the Bun model (included and curated, not assembled from a dependency hunt) and the Zig model for builds (build.cljgo is a program, not a data file). They arrive as real runnable templates, not docs:

cljgo new myapp # library (the default)
cljgo new --template cli myapp # a CLI tool
cljgo new --template web myapp # a bri web app

The web template scaffolds the bri stack — config, HTTP, HTML, auth, db, otel — with cljgo dev / cljgo test as the dev loop. Honest caveat: bri web apps run interpreted today; their AOT-to-static-binary tier is in progress (the compiler spike already produced a ~15 MB scratch Docker image with ~30 ms cold start).

If the compiler can't handle something, does it fall back to interpretation?

No. Compilation compiles or fails — there is no interpreter in the binary to fall back to. (Interop members the compiler can’t type-infer yet fall back to reflection within compiled code, with a compile-time note.) The interpreter exists on the other side: it is the REPL and macro engine, and every conformance test runs through both paths — REPL-vs-binary divergence is a release blocker.

Do I have to manually coerce types between Clojure and Go?

No. Values crossing into Go coerce automatically — a Clojure vector passes where a Go slice is expected, a Clojure fn passes as a typed Go callback — using the same table in both the REPL and compiled code (Glojure’s coercion table is the acknowledged model). The compiled path goes one step further: when the static type already matches, no coercion code is emitted at all — it’s a direct typed Go call. That is where the lower overhead comes from.

Does a real REPL work with this approach? With persistent state?

Yes — live re-def, defmacro at the prompt, nREPL for CIDER/Calva. Session resume is form replay, not a heap snapshot: successful top-level forms are saved (under ~/.config/cljgo/sessions) and replayed by cljgo repl :resume <id> (or :resume <id> at the prompt). See the REPL guide.

How readable is the emitted Go?

Honestly: not very, today. It is correct, gofmt-clean, and fast — but it is compiler output, not idiomatic hand-written Go. Readability improves as more of the emitter moves from boxed to typed emission; it is a non-goal to make it look hand-written.

What about Java interop — can existing Clojure libraries work via shims like gojava?

cljgo has no Java interop layer, by design — no JVM bytecode, no jars, ever. The Go ecosystem is the standard library. That means Clojure libraries that lean on Java classes don’t run as-is today; pure-Clojure libraries do. Projects like gojava (shimming common Java classes onto Go) are an interesting route for that gap and worth watching, but cljgo doesn’t use them yet.

Can I publish one library to both ecosystems — Go and Clojure?

Yes — this is a design goal, and both targets publish from the same build.cljgo, no second manifest:

cljgo publish go # a go-gettable Go module
cljgo publish clojars # pure Clojure source for JVM-Clojure consumers

Consuming from a Clojure project — it’s a git coordinate in deps.edn today (a real Clojars coordinate is on the roadmap):

{:deps {io.github.you/your-lib
{:git/url "https://github.com/you/your-lib"
:git/sha "abc123…"}}}

Consuming from a Go project — it’s a normal Go module:

go get github.com/you/your-lib

(Wrappers currently expose any signatures; typed signatures from type hints are a tracked follow-up.)

When it fails, and how: purity decides. A pure-Clojure library reaches both worlds. The moment any reachable namespace uses Go interop (require-go), the library is Go-side only — publish clojars refuses and names the offending file and line, instead of shipping the JVM a broken download. The check runs over the whole transitive required surface at publish time. Full details in dependencies & publishing.

Can one source file serve both JVM Clojure and cljgo?

Yes. .cljc is an accepted source extension with a :cljgo reader-conditional feature, so one file branches #?(:clj … :cljgo …) — the same pure .cljc lib runs on the JVM and on cljgo, and publishes both ways as above.

Why not just GraalVM native-image?

native-image is excellent and if your world is JVM libraries, use it. cljgo answers a different question: what if the host ecosystem is Go? You get the Go module universe as the standard library, go build compile times, trivial cross-compilation (pure-Go programs build for any OS/arch with no target toolchain), and no JVM anywhere in the toolchain. The trade is the one above — no Java interop.

How does it compare to babashka?

Different tools. babashka is a mature interpreter with a huge curated library set — for scripting it is the obvious choice. cljgo’s interpreter is a tree-walker and loses to bb; its compiled binaries are the point: on the benchmark suite, compiled cljgo wins the recursion and data-structure rows outright but honestly still loses reduce and transducers to babashka and let-go. Numbers and methodology on the benchmarks page.

Do macros work in compiled code? What about eval?

Macros work fully — the tree-walk evaluator is the macro engine, so macros (including your own) expand at compile time exactly as they do at the REPL. What compiled binaries do not carry is runtime eval/defmacro-at-runtime: zero interpreter is linked, by design. If you need runtime eval, that’s what the REPL side is for.

Does core.async work?

Yes, over real goroutines — go blocks are goroutines, channels are backed by Go channels, no CPS rewrite. Same semantics interpreted and compiled. See concurrency & core.async.

Can I call C?

Through Go, today: cgo-based Go modules (sqlite drivers, sensors, GUI/audio) import like any other module. Direct C FFI without cgo (purego, dlopen/dlsym, REPL-live) is designed and spiked but not landed yet.

How big and how fast are the binaries?

Hello-world compiles to a 7.1 MB static binary that starts in ~5 ms — the smallest binary of the three Clojure-on-Go AOT compilers (Glojure 19.0 MB, let-go 12.8 MB, all rebuilt and measured 2026-08-02), no JVM, no runtime install. Startup is a three-way tie in single-digit milliseconds; the 2026-07-31 claim that Glojure started ~1 ms quicker did not reproduce against freshly built binaries and has been withdrawn. Emitted code currently runs within ~3.8× of hand-written Go on the worst measured hot loop, and that gap is CI-gated so it only shrinks. Full tables on the benchmarks page.

Is it production-ready?

No. It is 0.x and moving fast. The parts that are solid are guarded hard (conformance against JVM 1.12.5, dual-harness REPL/binary parity, CI perf gates); the honest gap list is the section right above this FAQ.

Why build a fourth Clojure-on-Go?

Personal, honestly: day-to-day work moved to Go, and this is one tool that keeps Clojure — the real thing, verified against JVM 1.12.5, not a lookalike — while shipping normal Go binaries.

Questions or feedback on this page? Comment below with your GitHub account — comments are public and live in the project's GitHub Discussions.