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:
- Resolves the target triple from
GOOS/GOARCH(and sub-arch variables likeGOARM,GOAMD64). - Picks the compiler binary for the target's CPU family:
cmd/compileis a single binary that compiles for every supported architecture; the target arch is selected by command-line flags it receives fromcmd/go. - Picks the assembler (
cmd/asm), same shape — one binary, target-selected at invocation. - Picks the linker (
cmd/link), again one binary that knows every output object format (ELF, PE, Mach-O, wasm). - Selects per-platform source files in
runtime,syscall,internal/poll,os, andnetusing build constraints baked into file names and//go:buildlines.
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_BOOTSTRAPpoints at the bootstrap installation when you runmake.bashinsidesrc/.
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:
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.govsfd_windows.go— non-blocking I/O abstraction.src/net/dnsclient_unix.govsdnsclient_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:
- Run a C compiler to compile each
.cand.cgotranslation unit for the target. - Produce object files in the target's ABI/format.
- 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.ReadBuildInfoandgo 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 (
toolchaindirective ingo.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 tablecmd/compile: https://pkg.go.dev/cmd/compilecmd/link: https://pkg.go.dev/cmd/link- Installing Go from source: https://go.dev/doc/install/source
GOAMD64microarchitecture levels: https://go.dev/wiki/MinimumRequirements#amd64runtime/debug.ReadBuildInfo: https://pkg.go.dev/runtime/debug#ReadBuildInfo