If your go.mod requires example.com/lib v1.2.0 and v1.9.0 is now released, which version does `go build` use?
answer
- a require line is a floor
- publishing a tag moves nobody's build
- highest required, not highest available
- go.mod is read, tags are not
- upgrades are an edit, not a side effect
basics
~20 sThe build uses v1.2.0. Go's minimal version selection takes the highest version some go.mod in the graph actually requires, never the newest one published. A new release stays invisible to the build until a require line names it.
solid answer
~40 sIt builds with **v1.2.0**. Go resolves dependencies with *minimal version selection* (MVS): a `require` line is a **minimum**, and the version chosen for each module path is the maximum of the minimums stated across the main module and every module in its graph — not the maximum of what exists upstream. The go command does not query the proxy for newer tags during an ordinary build, so publishing v1.9.0 changes nothing about my build; the same source produces the same build list today and next year. Moving up is an explicit, reviewable edit to `go.mod` rather than something that happens because time passed. The tradeoff is the other direction: I never get a fix for free either, so raising minimums has to be a scheduled activity rather than a side effect of building.
code
mod · 5 linesmodule example.com/app
go 1.25
require example.com/lib v1.2.0 // still selected after v1.9.0 shipsgo deeper
Recall the one-sentence rule: a require line is a minimum, and Go builds with the highest minimum anybody required, not the newest release. Be able to say that publishing a new tag does not change an existing build.
Explain where the maximum is taken — across the main module and every module in the graph — and why that makes the build list a pure function of the source, so no lock file is needed for version selection.
Show the operational consequence: builds never drift, so nothing pulls in fixes for you. Be ready to describe how your team schedules and reviews minimum raises, and how you check the selected list against what is published.
Frame the tradeoff you are buying: determinism and reviewable upgrades in exchange for silent staleness. Be able to argue when that default serves a fleet of services and what process has to exist alongside it.
## The short answer `go build` uses **v1.2.0**. The freshly published v1.9.0 has no effect at all until something in the module graph says it wants it. ## What a `require` line means In `go.mod`, a line like ``` require example.com/lib v1.2.0 ``` is **not** "give me exactly v1.2.0" and **not** "give me v1.2.x, latest wins". It is a **lower bound**: "this module needs at least v1.2.0". Go's algorithm for turning those bounds into a concrete set of versions is called **minimal version selection**, usually written MVS. ## How MVS decides The go command builds a **module graph**: the main module points at the module versions it requires, each of those points at the module versions *their* `go.mod` requires, and so on. For every distinct module path in that graph, MVS selects the **maximum of the minimum versions that were required**. The resulting set of one version per module path is the **build list** — what actually compiles. The word "minimal" is about the *choice*, not about the number being small: MVS picks the smallest version that still satisfies every stated requirement. If your graph mentions only v1.2.0, the smallest version satisfying everything is v1.2.0. Crucially, **the set of published tags is not an input**. During a plain `go build`, `go test` or `go run`, the go command has no interest in what the upstream repository has tagged since. It reads `go.mod` files, computes the maximum per module path, and stops. v1.9.0 could have shipped an hour ago or a year ago; nothing in the graph names it, so it is not in the build list. ## Why the language does it this way Most other package ecosystems resolve *ranges* — "^1.2.0" means "newest 1.x you can find" — and therefore need a lock file to pin the answer, because the answer changes whenever someone publishes. Go inverts this: because the inputs are only the `go.mod` files in the graph, the answer is already deterministic, and a lock file for *versions* is unnecessary. (`go.sum` exists, but it records cryptographic hashes of module content, not which version was chosen.) The practical consequences are worth stating plainly: - **Reproducibility for free.** A checkout from two years ago builds with exactly the versions it built with then, on a machine that has never seen the code before. - **No surprise upgrades.** A dependency author cannot move your build by tagging a release. Nobody's CI turns red overnight because an upstream v1.9.0 changed behaviour. - **You must upgrade deliberately.** The flip side: a bug fix or a security fix published upstream does *nothing* for you until a `require` line in your graph names a version that includes it. Teams that never raise their minimums silently sit on old code, and "it built fine" is not evidence that they are current. - **A high-fidelity build.** You test the versions you ship, because the versions you ship are the ones written down. ## How the version does move Only two things raise a selected version: 1. **You raise it** — an explicit change to the main module's `go.mod`, which the go command will write for you when you ask it for a specific version (`go get example.com/[email protected]`). 2. **Something in the graph raises it** — you pull in a dependency whose own `go.mod` requires a higher version of `example.com/lib`, and MVS then takes that higher number as the new maximum. That second path is the only way you can end up on a version you never typed, and it is still fully explained by the maximum-of-minimums rule. ## Checking what you actually got `go list -m all` prints the selected build list — one line per module path with the version MVS chose. `go list -m -versions example.com/lib` prints the versions that *exist* upstream. Comparing those two is the honest way to answer "are we behind?", and the difference between the two lists is exactly the difference between what is published and what MVS selected.
- So how does a version you never typed ever end up in your build?Through the graph. If some dependency's own `go.mod` requires `example.com/lib v1.5.0`, that requirement is part of the maximum, so v1.5.0 is selected even though your go.mod says v1.2.0. `go mod tidy` then records it in your go.mod as an `// indirect` require. It is still maximum-of-minimums — the higher minimum simply came from somebody else's file.
- Does Go need a lock file to make builds reproducible?No, not for version selection. The inputs to MVS are only the `go.mod` files reachable in the graph, so the selected build list is already a pure function of the source — nothing upstream can change it. `go.sum` is still required, but it pins content *hashes* so the bytes for a chosen version cannot change underneath you; it does not decide which version is chosen.
- What is the operational downside of this design?You never drift upward, which also means you never pick up fixes by accident. A service can sit on a two-year-old dependency indefinitely and every build stays green. So raising minimums has to be an owned, scheduled activity with someone accountable for it, rather than something the build does for you.
A require line is like a minimum age on a ride: it says who may not board, not that the oldest person in the queue gets on.
saying these in an interview costs you the question
- Says Go picks the newest compatible release like other ecosystems
- Thinks a require line pins an exact version and nothing else can raise it
- Believes go build queries the proxy for newer tags
- Claims Go needs a lock file to be reproducible
- Confuses go.sum with a record of selected versions