skip to content

Minimal Version Selection

Go picks the lowest version that satisfies every requirement in the graph, not the newest release, so a build is reproducible without a lockfile and an upgrade is always something you asked for.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

If your go.mod requires example.com/lib v1.2.0 and v1.9.0 is now released, which version does `go build` use?

level: juniorimportance: must knowfreq 62%

answer

  1. a require line is a floor
  2. publishing a tag moves nobody's build
  3. highest required, not highest available
  4. go.mod is read, tags are not
  5. upgrades are an edit, not a side effect

basics

~20 s

The 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 s

It 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 lines
mod
module example.com/app

go 1.25

require example.com/lib v1.2.0 // still selected after v1.9.0 ships

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

In Go, if two dependencies require v1.2.0 and v1.5.0 of the same module, which one is selected and why?

level: middleimportance: should knowfreq 52%

basics

~20 s

v1.5.0 is selected. Minimal version selection walks the whole module graph and, for each module path, takes the maximum of the minimum versions required by any module in it. One version per path ends up in the build list.

open as a page

What is a Go pseudo-version like v0.0.0-20060102150405-abcdef123456, and when does the go command create one?

level: middleimportance: should knowfreq 44%

basics

~20 s

A pseudo-version is a synthetic semantic version the go command derives for a commit that has no suitable version tag. It encodes a base version, the commit's UTC timestamp, and a 12-character commit hash prefix, so untagged commits still order correctly.

open as a page

Your Go build still selects v1.4.1 of a transitive module although v1.4.3 exists — why, and how do you confirm it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Because no go.mod in the module graph names v1.4.3. Minimal version selection reads requirements, never the upstream tag list, so a published release is invisible until something requires it. Compare the selected build list against the versions that actually exist.

open as a page

In Go, what happens to the rest of the module graph when you downgrade one dependency to an older version?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Other modules move too. Because a selected version is the maximum of stated minimums, the go command must also downgrade or drop any module whose own go.mod requires a higher version of your target, and it never upgrades anything to compensate.

open as a page