Zero-binding Go interop
Interop is cljgo’s #1 design goal: the Go ecosystem is the standard
library. require-go pulls in a Go package and you call it directly — no
bindings, no wrappers, no generated stubs. The same forms run identically
interpreted and compiled (that parity is conformance-tested; see
the REPL guide).
Packages are namespaces
Section titled “Packages are namespaces”(require-go '[strings])(require-go '[math])
(strings/ToUpper "hi") ; => "HI"(strings/Repeat "ab" 3) ; => "ababab" (int64 args coerce to Go int)math/Pi ; consts and vars work in value positionAliases work the way you expect: (require-go '[net/url]) gives you a
url/ prefix; (require-go '[net/http :as http]) gives you http/.
Go’s export rule is part of the surface — you call strings/ToUpper,
not strings/to-upper. cljgo does not rename or re-case Go identifiers.
Errors are values; ! unwraps
Section titled “Errors are values; ! unwraps”A Go function returning (T, error) shapes to a [v err] vector. The
error slot is nil-normalized, so it is directly truthy-testable:
(require-go '[strconv])
(strconv/Atoi "123") ; => [123 nil](first (strconv/Atoi "x")) ; => 0 (Go's zero value passes through)(if (get (strconv/Atoi "x") 1) :parse-error :ok) ; => :parse-errorAppending ! to the call unwraps the value or throws — the 90% path.
! can never appear in a Go identifier, so the suffix is unambiguous
sugar:
(strconv/Atoi! "42") ; => 42(strconv/Atoi! "not-a-number") ; throws, carrying the Go errorBoth shapes are frozen in the conformance suite
(conformance/tests/interop-verr.clj, interop-bang-throw.clj).
Methods, fields, constructors
Section titled “Methods, fields, constructors”Clojure’s dot forms, verbatim:
(require-go '[net/url])
;; construct: (pkg/T. {:Field v}) builds a *url.URL — &url.URL{...}(def u (url/URL. {:Scheme "https" :Host "x"}))
;; field write: (set! (.-Field r) v)(set! (.-Host u) "y")
;; zero-valued pointer: (go/new pkg/T) — new(url.URL)(def z (go/new url/URL))
[(.-Scheme u) (.-Host u) (.-Path z)] ; => ["https" "y" ""](require-go '[strings])
(def r (strings/NewReplacer "a" "1" "b" "2"))(.Replace r "abcab") ; method call => r.Replace("abcab") => "12c12"Field reads use (.-Field r) — (url/Parse! "https://example.com/a/b")
followed by (.-Host u) gives "example.com"
(conformance/tests/interop-field.clj). Method and field access resolves
reflectively at runtime in both modes today (receiver-typed direct
emission is later roadmap work), so behavior is byte-identical by
construction.
Third-party modules
Section titled “Third-party modules”Any module on the Go proxy links with zero hand-written bindings. Declare
it in your project’s build.cljgo — cljgo synthesizes the go.mod, runs
go get, and the emitter resolves calls from real go/packages type
facts (this is examples/build-websocket in the repo):
;; build.cljgo(defn build [b] (let [app (exe b {:name "wsclient" :main "src/main.cljg"})] (go-require app "github.com/gorilla/websocket" "v1.5.3") (install b app) (run b app)));; src/main.cljg(require-go '["github.com/gorilla/websocket" :as ws])(println "close-normal code:" ws/CloseNormalClosure)(def frame (ws/FormatCloseMessage ws/CloseNormalClosure "bye"))Honest limit: the interpreter (cljgo repl / cljgo run) can only
see Go packages compiled into it — the stdlib works everywhere, but a
third-party module is reachable only in a compiled build. The
interpreted leg hard-errors on an unlinked package rather than
silently returning nil — never a wrong answer, always a loud one
(ADR 0053):
$ cljgo run tp.cljerror: go module github.com/google/uuid is not linked into the interpreter (accessing member NewString) (at tp.clj); build it (cljgo build)help: run `cljgo explain G5000`cgo-based Go modules (sqlite drivers, sensors, GUI/audio) import like any other module — they are just Go modules — though they constrain cross-compilation (see Compile & ship).
Not supported (yet, or ever)
Section titled “Not supported (yet, or ever)”- Java interop — never. cljgo compiles to Go, not JVM bytecode.
JVM-surface forms like
(System/getProperty …)orimportfail loudly at analysis with a named error, not silently. - C FFI (
ffi/deflibon purego) — designed (ADR 0044) but not implemented. Today C is reached via cgo-based Go modules. - Go generics — no
go/instantiateform exists yet. Among thego/interop operators, onlygo/newis implemented. - ns-form
:require-go— use the function form(require-go '[...]); that is what every shipped example and conformance test uses.
Channels received from Go APIs participate in core.async directly — see Concurrency & core.async.
Questions or feedback on this page? Comment below with your GitHub account — comments are public and live in the project's GitHub Discussions.