# Go - Toolchain Pinning and the Build Cache
Every tool in a Go build chain resolves a Go version on its own: the `go` command, golangci-lint, govulncheck, and archive builders each pick or download one. Pin each version explicitly, and re-align the local toolchain when CI pins a different one. `go build` never compiles `_test.go` files, so `go build ./...` is the ground truth for compile state, and the build cache can replay stale tool output.
## Key points
- `GOTOOLCHAIN=local` in CI forces the runner to use exactly the pinned Go version instead of silently downloading a newer one. An old runner fails after a minimum-version bump instead of upgrading itself. Use `stable` or `oldstable` in `actions/setup-go` for automatic tracking. CI benchmarks without `actions/setup-go` need explicit version pins.
- After a `go` directive bump, every `go.mod` and `go.sum` in a multi-module repo needs an update. One make target sweeps all modules at once.
- golangci-lint must be built with a Go at least as new as the code it analyzes. When the stock binary cannot decode the current toolchain's export data, its `declared and not used` cascades are import-decode artifacts, not findings. Rebuild the repo-pinned version from the tools module with the local Go: `GOWORK=off GOBIN=/tmp/gclbin go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint`.
- Run a linter binary only under the Go it was built with. A linter built with Go 1.26 panics on a Go 1.27 stdlib: `GOTOOLCHAIN=go1.26.10 gmake lint/go`.
- golangci-lint serializes itself. Two concurrent invocations exit with `parallel golangci-lint is running`, so overlapping lint runs fail on the runner itself.
- Set `GOTOOLCHAIN` to CI's version before `go mod tidy` (`GOTOOLCHAIN=go1.26.0 go mod tidy`), so local output matches CI's older toolchain.
- A root `go.mod`'s `toolchain` directive pins CI and release builds only because every setup-go step resolves through `go-version-file: go.mod`. Direct `go` runs in a submodule read only that module's directive, and the go command ignores `toolchain` directives in dependencies. Downstream users get no coverage.
- Archive bytes depend on the Go version that runs the build tool. A bundle built under a newer local Go differs from CI's build. Regenerate with the CI toolchain before comparing archives.
- `GOTOOLCHAIN=local` unblocks `go generate` when the toolchain download fails mid-build and a bootstrap Go is present.
- A Go bindings library on internal APIs of one minor version (1.23.x, removed in 1.24) must pin the Docker base image's Go version, not the host toolchain.
- `go vet` compiles `_test.go` files, `go build` does not. A non-test file that references a test-only symbol passes vet and test while the plain build of the module fails. Trace the symbol to its `_test.go` definition, then run one targeted `go build`.
- `go build -gcflags='-S'` prints nothing for benchmark code because `go build` skips `_test.go` files. Use `go test -gcflags='-S' -run XXX -bench XXX .`.
- `import "C"` in a `_test.go` file is rejected by the filename suffix, not the package. `go/build` computes `isTest` from the name and gives the external `_test` package no carve-out (golang/go#4030, deliberate since 2012). Put the cgo in a normal file and test it from there.
- The build cache replays `toolexec` stderr (golang/go#27628, still reproducing on Go 1.27.1). After a cold `go build -toolexec=/usr/bin/time ./app`, a later plain `go build` in the same `GOCACHE` replays the cached stderr. `go build -x` then shows only cache `cat` lines, no compile or link actions.
- Hand-rolled `GOFLAGS` parsing diverges from `cmd/internal/quoted.Split`. A parser that splits only on space and tab keeps a newline-separated `-toolexec` entry in one token, so it survives stripping and recurses the build. The space-separated `-toolexec <cmd>` form is invalid in `GOFLAGS` (`non-flag` error).
- One undefined symbol can hide a whole migration. Go reports type errors per package. When one package fails, its dependents are never type-checked, so an interface rework stays invisible behind the single error. The fix needs a `go build ./...` sweep.
- `go build ./...` is the authoritative ground truth for mid-rebase compile state. LSP diagnostics are stale during active conflict resolution. Ignore them until after staging.
## How to apply
1. Pin CI with `GOTOOLCHAIN=local` plus a version pin in `actions/setup-go`, or the `stable`/`oldstable` alias when tracking is wanted.
2. Align the local toolchain to CI before lint, tidy, or archive work: `GOTOOLCHAIN=go1.26.10 gmake lint/go`, or `PATH=$HOME/sdk/go1.26.8/bin:$PATH GOTOOLCHAIN=local GOFLAGS=-p=2 GOMAXPROCS=2`.
3. Reproduce a CI lint failure with the repo-pinned linter binary, not the system one.
4. Check compile state with `go build ./...`, never with `go vet`, `go test`, or LSP diagnostics alone.
## Observations
- [tooling] Every tool in a Go build chain resolves a Go version on its own. Pin each one. #toolchain (source: agent sessions, 2026-10)
- [tooling] golangci-lint must run under the Go it was built with. Rebuild the pinned version when export data cannot be decoded. #golangci-lint (source: agent sessions, 2026-10)
- [build] `go build` never compiles `_test.go` files. It is the ground truth for compile state. #go-build (source: agent sessions, 2026-07)
- [cache] The build cache replays `toolexec` stderr on a later plain build (golang/go#27628). #build-cache (source: agent sessions, 2026-10)
- [portability] A library on internal APIs of one Go minor version must pin its build image's Go version. #portability (source: agent sessions, 2026-01)
## Relations
- relates_to [[resources/documentation/Go/Go - Modules, Workspaces, and GOPROXY]] (go.mod directives and multi-module layout decide what each go run resolves)
- relates_to [[resources/documentation/Go/Go - Lint and Vulnerability Tooling]] (linter binaries must match the toolchain they analyze)
- relates_to [[resources/documentation/Go/Benchmarks/Go Benchmarks - Compiler Honesty and Dead Code Elimination]] (go build skips _test.go files, so benchmark code needs go test to compile)
## Sources
- Personal agent-session notes, January 2026 to October 2026.