Skip to main content

07. Go target and portability

This note describes the Go-toolchain-and-target matrix the transpiler ships against.

Go version floor and gate

  • Floor: Go 1.21 (Aug 2023). First version with stable GOOS=wasip1. We need wasip1 for Phase 17. Below 1.21 we would only ship GOOS=js for the browser case, which excludes wasmtime / wasmer / WasmEdge.
  • Production gate: Go 1.26.0 + Go 1.26.x latest. The active stable as of 2026-05.
  • Forward-compat smoke: Go 1.27 beta. Runs as allow_failure: true to catch upcoming breaking changes.

We do not gate against Go 1.18 / 1.19 / 1.20 because they predate the slices and maps stdlib packages and the reproducible-build defaults. Supporting them would mean reimplementing those helpers in the runtime module, doubling LOC for negligible reach.

OS / arch matrix

Go's first-class cross-compile via GOOS + GOARCH env vars is the load-bearing feature for the Phase-17 targets. The matrix:

TargetGOOSGOARCHLinkerNotes
TargetGoBinaryLinuxAmd64linuxamd64internalDefault Linux server target.
TargetGoBinaryLinuxArm64linuxarm64internalRaspberry Pi 4+, AWS Graviton.
TargetGoBinaryDarwinAmd64darwinamd64external (ld64)Intel Macs.
TargetGoBinaryDarwinArm64darwinarm64external (ld64)Apple silicon.
TargetGoBinaryWindowsAmd64windowsamd64internalMost Windows servers and desktops.
TargetGoBinaryFreeBSDAmd64freebsdamd64internalThe "is this still maintained" target.
TargetGoWasmJSjswasminternalBrowser via wasm_exec.js.
TargetGoWasiP1wasip1wasminternalwasmtime / wasmer / WasmEdge.

All targets except darwin link with Go's internal linker — no external toolchain required. The darwin targets need Apple's ld64, which is installed by xcode-select --install on macOS; on Linux developers wanting to cross-compile to darwin install osxcross (out of scope for the MEP).

The Linux arm64 target is gateable from an Ubuntu x86_64 CI runner because Go's internal linker handles cross-arch without a special linker. This is the killer cross-compile feature.

Cgo across the matrix

import "C" works on every target where Go has a host C compiler available:

Targetcgo
linux / amd64yes (gcc)
linux / arm64yes (with cross-gcc)
darwin / amd64, arm64yes (clang)
windows / amd64yes (MinGW)
freebsd / amd64yes (clang)
js / wasmno
wasip1 / wasmno

Phase 12 fixtures skip cgo when the target is js/wasm or wasip1/wasm. The skip is at fixture-discovery time, not build-failure time.

Wasm: the js vs wasip1 split

GOOS=js GOARCH=wasm targets the browser. Produces a .wasm that requires wasm_exec.js (shipped with the Go toolchain at $(go env GOROOT)/lib/wasm/wasm_exec.js) as the JS-side glue. The wasm_exec.js provides the syscalls (fs.read, fs.write, time.now, etc.) the wasm module needs.

GOOS=wasip1 GOARCH=wasm targets the WASI Preview 1 standard. Produces a .wasm that runs under any wasmtime / wasmer / WasmEdge host with no extra glue. wasip1 was renamed from wasi in Go 1.21 (matching the upstream naming change).

Differences:

Featurejs/wasmwasip1/wasm
os.ReadFilerequires fs.readFile shim in wasm_exec.jsworks directly under wasmtime's --dir=...
net/http servernot possible (no net.Listen)not possible (wasip1 has no socket API)
net/http clientworks via the browser's fetch API + a shimnot possible (wasip1 has no socket API)
time.Nowworks via Date.now()works via wasi's clock_time_get
runtime.GOMAXPROCSalways 1always 1
cgonot supportednot supported
goroutinescooperative; single OS thread; goroutines yield via scheduler pointssame

The cooperative-single-thread constraint means streams and agents work but never parallelise. Phase 9 / 10 fixtures targeting wasm exclude any parallelism assertion.

Vendor vs proxy mode

By default the driver writes vendor/dev.mochilang/runtime/go/... into every emitted module and invokes go build -mod=vendor. This makes the build offline-capable: no proxy.golang.org contact required.

Driver.NoVendor=true switches to proxy mode. The emitted go.mod lists dev.mochilang/runtime/go v1.x.y as a normal require; go build resolves the dependency from ~/go/pkg/mod (filling from the proxy if needed). This is faster for incremental builds (no vendor copy) but requires a populated module cache.

Why no TinyGo for the default path

TinyGo (2018-present) is a separate Go compiler producing LLVM IR; it targets microcontrollers (ATSAMD, STM32, ESP32, RP2040) and wasm with smaller binaries than gc. TinyGo binary size on wasm32 is typically 5-50x smaller than gc.

The trade: TinyGo does not support the full Go stdlib. Notable gaps:

  • No reflect.Type.NumMethod, no reflect.Value.Call. Breaks any reflection-based helper (mochiruntime.AnyEqual uses reflect.DeepEqual).
  • Limited encoding/json (works but slow; no streaming decoder).
  • No net/http server. Limited net/http client (depends on target).
  • Limited goroutines (cooperative scheduling, no preemption). Channels work.
  • No cgo (on most targets).

For Phase 17 wasm fixtures we hit at least the reflect gap. So TinyGo is out for the default. It is a candidate sub-target for Phase 17.x once the source-language surface that uses reflect is gated off.

CI matrix

The phase gate runs in CI as a matrix of (OS, Go version):

strategy:
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
go: ['1.26.0', '1.26.x']

The cross-arch targets (linux/arm64, darwin/amd64, etc.) are exercised from the ubuntu-24.04 runner via GOOS=... GOARCH=... go build cross-compile. The compiled binary is not executed there — execution gating for cross-arch is Phase 16.x.

The wasm gate uses wasmtime 26 installed via cargo install wasmtime-cli; if absent the wasm fixtures skip.

Reach by region

Go's installed base is concentrated in the cloud-native ecosystem. CNCF's 2025 annual survey reported >70% of CNCF-graduated projects are dominantly Go. Go is also the default Lambda Custom Runtime language (AWS), the default Cloud Functions language (GCP), and the default Azure Function language (Microsoft) for low-cold-start serverless. A Mochi program compiled to Go reaches every cloud where serverless is a deployment option.

Reach by deployment shape

Deployment shapeGo fitNotes
Single static binary on Linux serverA+The default go build produces this.
Sidecar in a Kubernetes podA+Same. Plus the entire k8s API is Go.
Lambda functionAGOOS=linux GOARCH=arm64 go build produces a Lambda-compatible binary for the arm64 runtime.
Cloud Run / Knative serverlessADistroless container with a single binary.
Edge workerB+wasip1 fits Cloudflare Workers' WASI runtime; latency varies.
Browser web appCWasm binary size (~3-5MB minified with gc) is heavy for browsers; TinyGo helps but loses reflect.
Mobile (iOS / Android)Cgomobile bind exists but mobile is better served by MEP-49 (Swift) and MEP-50 (Kotlin).
Embedded MCUDTinyGo only; out of scope for default path.