Skip to content

SYSTEM Cited by 2 sources

Go compiler

The Go compiler (go build front-end, cmd/compile) compiles each required package into its own intermediate artifact (.a) that the Go linker later joins into a single binary. Works at package granularity, not file granularity.

File selection rules

For a given package, the compiler includes every non-test file (not ending in _test.go) whose build constraints are satisfied. Constraints can depend on:

  • OS (GOOS=linux, GOOS=darwin, …)
  • Architecture (GOARCH=amd64, GOARCH=arm64, …)
  • Explicit build tags passed to go build -tags t1,t2
  • Compiler identity (gc vs gccgo)
  • Go version (go1.24)
  • CGO enablement (cgo / !cgo)
  • Architecture features

Package selection (transitive)

Starts from the main package; transitively adds every import it encounters in files that aren't excluded by build constraints. Also includes the stdlib runtime package + its internal dependencies (unavoidable — every Go binary needs them).

Important: a file excluded by build tag does not contribute its imports. This is one of the two principal ways dependencies are pruned in Go. The other is moving symbols into a separate package so only binaries that explicitly import that package pull in its deps (patterns/single-function-forced-package-split).

Listing included packages

go list -f '{{ join .Deps "\n" }}' -tags t1,t2 ./path/to/main (with the appropriate GOOS/GOARCH env vars) prints the full transitive package set that ends up in the binary. This is the what — for the why, use goda; for the cost, use go-size-analyzer.

Side effects of import

Per Datadog: "simply importing a package has side effects: init functions run and global variables are initialized, which can be enough to force the linker to keep many unnecessary symbols." Canonical wiki instance: importing the stdlib plugin package (concepts/go-plugin-dynamic-linking-implication) reshapes the linker's entire dead-code policy even though no plugin code is exercised.

Seen in

Arm64 codegen and preemption safety

The compiler's arm64 backend (cmd/internal/obj/arm64/obj7.go) emits function prologues + epilogues that adjust the stack pointer by the frame size. For frames > 1<<12 bytes, the IR pre-go1.23.12 expressed this as a single logical ADD $n, RSP, RSP and relied on the assembler to split the immediate into ADD $low, RSP, RSP plus ADD $(high<<12), RSP, RSP (the arm64 ISA's 12-bit ADD immediate forces this). Between the two opcodes, RSP pointed into the middle of the stack frame.

Async preemption landing in that one-instruction window leaves the stack pointer partially adjusted — invalid for stack unwinding. At Cloudflare's 84 M req/s scale across 330 cities this produced ~30 fatal panics per day across <10 % of data centers. See concepts/split-instruction-race-window.

The fix — shipped in go1.23.12, go1.24.6, go1.25.0 — promotes the immediate-aware decomposition from the assembler to the compiler: for wide offsets the compiler now builds the offset in a scratch register (MOVD + MOVK on R27) and applies it via a single indivisible register-form ADD R27, RSP, RSP. Preemption can land before or after, never during. See patterns/preemption-safe-compiler-emit.

Last updated · 200 distilled / 1,178 read