Your CI's installed Go is older than a go.mod `go` line requires — what does the go command do?
answer
- the go line is a floor, not a label
- the default does not just fail
- one environment variable decides the policy
- local means refuse, auto means switch
- the compiler can come from go.mod
basics
~20 sBy default the go command does not fail: it downloads and re-executes with a newer Go toolchain that satisfies the go.mod requirement. Setting GOTOOLCHAIN=local disables that, and the command then refuses with an error naming the required and running versions.
solid answer
~50 sThe `go` line in go.mod is a **requirement**, not a note about what the author used: it states the minimum Go version needed to build the module. When the installed toolchain is older, the default behaviour (`GOTOOLCHAIN=auto`) is for the go command to select a newer toolchain — the `toolchain` line names one if present, otherwise the minimum that satisfies the `go` line — download it, and re-run the command with it. The build then succeeds under a compiler you never installed. `GOTOOLCHAIN=local` turns that off, and you get an error along the lines of `go.mod requires go >= 1.27 (running go 1.25; GOTOOLCHAIN=local)`. You can also pin a specific version, e.g. `GOTOOLCHAIN=go1.27.0`. The operational choice matters: auto keeps contributors unblocked when someone raises the `go` line, while local keeps a build image hermetic and makes a raised `go` line fail loudly instead of silently changing compilers.
code
text · 5 lines$ GOTOOLCHAIN=local go build ./...
go: go.mod requires go >= 1.27 (running go 1.25; GOTOOLCHAIN=local)
$ go env GOTOOLCHAIN
autogo deeper
Know that the go line in go.mod is a minimum requirement, not a note, and that a modern go command may fetch a newer toolchain for you rather than failing outright.
Explain the mechanism: the go and toolchain lines, GOTOOLCHAIN with its auto, local and pinned-version settings, and what the refusal message looks like when switching is disabled.
Diagnose it in a pipeline: recognise that a pinned build image no longer determines the compiler under the default, and know the three things to read — the go line, go env GOTOOLCHAIN, and the installed version.
Weigh hermetic builds against contributor friction: where switching is allowed, how a go-line bump is reviewed as an environment change, and what it costs an air-gapped pipeline to depend on fetching a toolchain.
## The go line is a floor Every `go.mod` has a `go` line. It is easy to read it as metadata — "this was written with Go X" — but it is a **requirement**: the module needs a Go toolchain at least that new to build. Raising it is therefore a change to every consumer's build environment, not a comment. Alongside it a module may carry a `toolchain` line, which names a specific toolchain to use when the locally installed one is not new enough. The two are different in kind: the `go` line says *what is required*, the `toolchain` line says *what to run* when the local toolchain falls short. ## What happens when the local Go is too old Modern Go toolchains do not simply give up. The behaviour is governed by the `GOTOOLCHAIN` environment variable: - **`auto` (the default).** If the installed toolchain is older than the module requires, the go command selects a suitable newer toolchain — the one named by the `toolchain` line if there is one, otherwise the minimum that satisfies the `go` line — downloads it, and re-executes itself with it. Your `go build` succeeds using a compiler the machine did not have a moment earlier. - **`local`.** No switching. The installed toolchain is the only one used, and if it is too old the command refuses with a message naming both versions, for example `go.mod requires go >= 1.27 (running go 1.25; GOTOOLCHAIN=local)`. - **A specific version**, such as `GOTOOLCHAIN=go1.27.0`, forces that toolchain regardless of what is installed. The important consequence for anyone operating a build: with the default, *the version of Go that compiles your code is a property of go.mod, not of your build image*. ## Why this surprises people in CI The symptom that brings this up in an interview is usually one of: - a build image pinned to a Go version quietly produces binaries built by a different one, so "which compiler built this artefact?" is no longer answerable from the Dockerfile; - a build in an air-gapped or proxy-restricted environment fails to fetch the toolchain, and the failure looks like a dependency error rather than a toolchain one; - a pull request that bumps the `go` line changes the compiler for every contributor and every pipeline at once, with no other visible change in the diff; - builds get slower or flaky the first time each runner has to download a toolchain. None of these are bugs. They are the design working: the go command's goal is that a module which requires a newer Go can still be built by whoever has an older one. ## Choosing a posture Both settings are defensible, and the choice belongs with whoever owns the build: **Prefer `auto`** on developer machines. It removes an entire class of "install a newer Go first" friction, and contributors do not need to track the repository's version by hand. **Prefer `local` in a hermetic pipeline**, with the intended Go version installed in the image. Then the toolchain is an explicit, reviewable part of the build environment, a `go` line bump fails loudly and is fixed by updating the image in the same change, and no build step depends on downloading a compiler at build time. Air-gapped builds effectively require this. Whichever you choose, be consistent: a repository where CI uses one Go version and contributors silently use another is where "it tidies differently on my machine" comes from, because commands like `go mod tidy` can produce different files under different toolchains. ## What to check when it bites - `go version` on the runner tells you what is installed, not necessarily what compiled the code. - `go env GOTOOLCHAIN` tells you the effective policy. - The `go` and `toolchain` lines of go.mod tell you what the module demands. Read those three together and the behaviour is fully determined: a required version, a policy, and an installed version. Any surprise is one of the three not being what you assumed.
- What is the difference between the `go` line and the `toolchain` line in go.mod?The `go` line is the minimum Go version required to build the module — a floor every consumer must meet. The `toolchain` line names which toolchain the go command should run when the locally installed one does not meet that floor. It is only meaningful when it is newer than what the `go` line already demands.
- Why would a platform team set GOTOOLCHAIN=local in CI?For hermetic, reproducible builds. The image's Go is then provably the compiler that produced the artefact, no build step depends on downloading a toolchain, and a raised `go` line fails loudly so the image is updated deliberately in the same change instead of the pipeline silently switching compilers.
- How can differing toolchains produce a noisy go.mod diff between contributors?Commands that rewrite go.mod, `go mod tidy` above all, can differ between Go versions — in the requirements they keep and in the file layout they write. If CI and contributors run different toolchains, the file flips back and forth across pull requests. Fixing the version everyone uses removes the churn.
saying these in an interview costs you the question
- Assumes the installed Go is always the compiler that runs
- Treats the go line as documentation of what the author used
- Raises the go line without checking what CI has installed
- Thinks a too-old toolchain always fails the build
- Cannot name the variable that controls toolchain switching