# 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.