Skip to content

Cross-compilation — Professional / Internals

1. How go build selects the per-target toolchain

When you run GOOS=linux GOARCH=arm64 go build ., the cmd/go driver:

  1. Resolves the target triple from GOOS/GOARCH (and sub-arch variables like GOARM, GOAMD64).
  2. Picks the compiler binary for the target's CPU family: cmd/compile is a single binary that compiles for every supported architecture; the target arch is selected by command-line flags it receives from cmd/go.
  3. Picks the assembler (cmd/asm), same shape — one binary, target-selected at invocation.
  4. Picks the linker (cmd/link), again one binary that knows every output object format (ELF, PE, Mach-O, wasm).
  5. Selects per-platform source files in runtime, syscall, internal/poll, os, and net using build constraints baked into file names and //go:build lines.

Everything is in one Go installation. No separate "SDK per platform" — the toolchain is structurally a cross-compiler with the host happening to be one target.


2. The bootstrap toolchain (historical and current)

The Go compiler is self-hosted: cmd/compile and friends are written in Go. To build a fresh Go from source you need an older Go to bootstrap.

  • Historically (Go 1.5 – Go 1.19) the bootstrap requirement was Go 1.4 (the last C-implemented compiler) or any newer Go.
  • From Go 1.20 the minimum bootstrap is Go 1.17.13, then raised to Go 1.20.x for Go 1.22+, then Go 1.22.x for Go 1.24, and so on (the policy is "bootstrap with the previous LTS-ish point release").
  • GOROOT_BOOTSTRAP points at the bootstrap installation when you run make.bash inside src/.

The script src/make.bash (and src/cmd/dist) drives the multi-pass build: it compiles dist with the bootstrap Go, then dist builds the cross-compiler set, then those compile the standard library for the target. dist list (the same one users call via go tool dist list) is part of that program.


3. cmd/dist and the platform table

src/cmd/dist/build.go carries an authoritative list of supported (GOOS, GOARCH) pairs and their port class:

  • First class — full test coverage, broken builds block releases (e.g., linux/amd64, linux/arm64, darwin/arm64, windows/amd64).
  • Second class — built and tested, but a regression is not a release blocker.
  • Broken / experimental — gated behind explicit acknowledgement.

go tool dist list -json returns this metadata structured so build systems can categorize targets:

go tool dist list -json | jq '.[] | select(.FirstClass==true)'

4. Per-GOOS runtime and syscall trees

Look inside $GOROOT/src/runtime and you see files like:

os_linux.go
os_darwin.go
os_windows.go
os_plan9.go
mem_linux.go
mem_darwin.go
defs_linux_amd64.go
defs_linux_arm64.go
signal_unix.go
signal_windows.go

The file-name suffixes (_linux, _darwin_arm64) are implicit build constraints. The compiler includes only the files matching the target. Higher-level packages mirror this:

  • src/syscall/zsyscall_<goos>_<goarch>.go — autogenerated raw syscall stubs.
  • src/internal/poll/fd_unix.go vs fd_windows.go — non-blocking I/O abstraction.
  • src/net/dnsclient_unix.go vs dnsclient_windows.go — DNS resolver.

This file-level dispatch is why pure-Go cross-builds work without #ifdefs: the right files are simply selected and compiled.


5. How the linker handles target-specific concerns

cmd/link is a single Go program that emits binaries in three families:

Family Targets What is target-specific
ELF linux, freebsd, netbsd, openbsd, dragonfly, solaris, illumos, android Section layout, dynamic-tag handling, relocation types per arch
PE/COFF windows Subsystem selection (console vs GUI via -H windowsgui), import tables, base relocations
Mach-O darwin, ios Load commands, code-signature placeholder, segment layout
Wasm js, wasip1 Module sections, table/memory imports, JS interop shims (js)

Relocations are encoded per arch (R_X86_64_PC32, R_AARCH64_CALL26, etc.); cmd/link knows them via internal tables keyed by GOARCH. The internal linker is the default; it falls back to the external system linker only when cgo or unusual link flags require it — which is why cgo and cross-compilation are friction-laden together.


6. Why cgo cross-compiling is hard

For cgo, the toolchain must:

  1. Run a C compiler to compile each .c and .cgo translation unit for the target.
  2. Produce object files in the target's ABI/format.
  3. Run a system linker (the external linker) to combine Go-emitted objects with C objects and platform libraries (libc, libm, …).

Step 1 and step 3 are not provided by the Go toolchain. You need CC/CXX set to a cross-compiler that emits target-correct code (gcc-aarch64-linux-gnu, zig-cc with -target …, clang with --target=…, mingw-w64 for Windows, osxcross for macOS-from-Linux). When CGO_ENABLED=1 you must also typically set CC_FOR_TARGET / CXX_FOR_TARGET when bootstrapping cross-compilers via make.bash.

The pragmatic rule: avoid cgo if you can ship pure-Go. If you can't, treat the cross C toolchain as part of your build infrastructure.


7. Build IDs, DWARF, and reproducibility

Each Go binary embeds:

  • A build ID (go.buildid) — a hash of the inputs that produced the link.
  • DWARF debug info — symbol locations, line tables, type info (unless stripped).
  • Build info — module path, version, and the flags read by runtime/debug.ReadBuildInfo and go version -m.

For byte-identical reproducible builds you must:

  • Clear the build ID: -ldflags="-buildid=".
  • Strip DWARF and symbol table when not needed: -ldflags="-s -w".
  • Trim source paths: -trimpath.
  • Disable VCS stamping: -buildvcs=false.
  • Pin the toolchain (toolchain directive in go.mod).
  • Sort module proxies and avoid time-dependent inputs (the toolchain itself does not embed timestamps when the above flags are set).

Concretely, cmd/link/internal/ld reads -B (the build ID) and -buildid, and when buildid is empty the section is omitted; this is what cosign and SLSA pipelines rely on to verify provenance independent of the builder.


8. Source-tree pointers

If you want to read the implementation:

src/cmd/dist/build.go         # platform table, bootstrap flow
src/cmd/dist/buildtool.go     # toolchain bootstrap
src/cmd/go/internal/work/     # build orchestration
src/cmd/compile/internal/<arch>/   # per-arch SSA lowering (amd64, arm64, riscv64, ...)
src/cmd/link/internal/ld/     # platform-agnostic linker
src/cmd/link/internal/<arch>/ # per-arch relocation handling
src/runtime/                  # per-GOOS/GOARCH files (file-name dispatch)
src/internal/syscall/<goos>/  # per-OS syscall helpers
src/internal/abi/             # ABI constants per arch

go tool dist test runs the per-port test set; failures there are how Go gates a release.


9. Sub-architecture knobs you only meet in production

Variable Targets Effect
GOAMD64=v1..v4 amd64 Minimum x86-64 micro-architecture level; higher levels emit SSE4.2/AVX/AVX-512 instructions
GOARM=5\|6\|7 arm (32-bit) Floating-point ABI: soft-float vs VFPv2 vs VFPv3
GOMIPS=hardfloat\|softfloat mips, mipsle FPU presence
GOPPC64=power8\|power9\|power10 ppc64, ppc64le Minimum ISA level
GOWASM=satconv,signext wasm Required wasm extensions

These exist because "arm" or "amd64" alone are not specific enough for some deployments (embedded ARMv6 vs ARMv7, AVX2-enabled servers vs old VMs). Mismatch causes SIGILL at runtime.


10. Summary

Go's cross-compilation is built into the standard distribution because the compiler, assembler, and linker are single Go binaries that already target every supported port. cmd/dist carries the supported-platform table, the runtime/syscall trees use file-name build constraints for per-platform dispatch, and cmd/link knows all three major binary formats. cgo cross-compiling is hard because it pulls in an external C toolchain and the external system linker. Reproducible cross-builds require clearing the build ID, stripping DWARF, trimming paths, disabling VCS stamping, and pinning the toolchain. Sub-arch variables (GOAMD64, GOARM, GOPPC64, ...) refine the target when "arch" alone is too coarse.


Further reading

  • src/cmd/dist/build.go — supported-port table
  • cmd/compile: https://pkg.go.dev/cmd/compile
  • cmd/link: https://pkg.go.dev/cmd/link
  • Installing Go from source: https://go.dev/doc/install/source
  • GOAMD64 microarchitecture levels: https://go.dev/wiki/MinimumRequirements#amd64
  • runtime/debug.ReadBuildInfo: https://pkg.go.dev/runtime/debug#ReadBuildInfo