skip to content

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%

answer

  1. gather every requirement, keep the largest
  2. depth in the graph does not matter
  3. one version per module path is linked
  4. two minimums are not a conflict
  5. maximum of minimums is the whole rule

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.

solid answer

~40 s

v1.5.0 wins. The go command loads the **module graph** — the main module's requirements, their requirements, and so on — and for each module path computes the **maximum of the minimums** stated anywhere in that graph. Two requirements on the same path never coexist: Go builds one version per module path, so the module that asked for v1.2.0 is compiled against v1.5.0 instead, relying on semantic-versioning compatibility within the major version. This is also the only mechanism by which a version you never wrote appears in your build, and after `go mod tidy` it typically shows up in your own `go.mod` as an `// indirect` require so the graph stays self-describing. `go list -m all` prints the resulting build list, which is what actually compiles.

code

mod · 5 lines
mod
// go.mod of example.com/a v1.0.0
require example.com/c v1.2.0

// go.mod of example.com/b v1.0.0
require example.com/c v1.5.0

go deeper

for a junior

Remember that requirements from dependencies count too, and the highest one wins. Know that only one version of a module path is ever linked into a program.

for a middle

Be able to describe the module graph as nodes of (module path, version) with requirement edges, and walk through computing the maximum per path to get the build list. Explain why this needs no backtracking.

for a senior

Show that you can explain an unexpected selected version to a teammate: trace which module version in the graph stated the higher minimum, and know which commands reveal it rather than guessing.

for a principal

Own the consequence for library authors on your side of the fence: because your published go.mod raises everyone's floor, the minimums you declare are a compatibility commitment to consumers, not a private preference.

## The rule in one line For every module path, the go command selects **the maximum of the minimum versions required by any module in the graph**. Here the minimums are v1.2.0 and v1.5.0, so **v1.5.0** is selected. ## The module graph Start from the main module — the one whose directory you are building in. Its `go.mod` lists module versions it requires. Each of *those* module versions has its own `go.mod` listing further requirements. Following those edges gives a directed graph whose nodes are (module path, version) pairs. This graph is the entire input to resolution; nothing else is consulted. Note what the nodes are: a *specific version* of a module contributes *its* requirements. If module A at v1.0.0 requires C v1.2.0 and module B at v1.0.0 requires C v1.5.0, then the graph contains both edges, and both are treated as claims about the minimum acceptable C. ## Computing the build list Minimal version selection sweeps the graph and, for each distinct module path, keeps the highest version required for it. The result — one version per module path — is the **build list**: exactly the set of module versions the compiler is given. That one-version-per-path property matters. Unlike ecosystems that can install two copies of a package at different versions in different subtrees, Go links **a single version of each module path** into a build. So A, which asked only for v1.2.0, is compiled against v1.5.0. Go's answer to why this is safe is the semantic-versioning promise plus the import-compatibility rule: within one major version, a later release is expected to be backward compatible, and if it is not, the author was supposed to change the module path (the `/v2` style suffix) rather than break v1 users. ## Why maximum, and why that is still "minimal" The algorithm's name confuses people. "Minimal" refers to the *selection*: among all version assignments that satisfy every stated minimum, MVS takes the lowest. Since a lower bound of v1.5.0 exists in the graph, anything below v1.5.0 fails to satisfy it, so v1.5.0 is the smallest satisfying choice. Taking the maximum of the minimums *is* choosing minimally. The practical property that falls out: **only requirements move versions**. Publishing does not. Time does not. Adding a dependency whose own `go.mod` demands more does — and that is a visible, reviewable event, because the requirement is written down in a file you fetched. ## The version you never typed This is worth internalising as the answer to "where did that come from?". Your `go.mod` might say C v1.2.0 while `go list -m all` reports C v1.5.0. Nothing is broken: some other module in the graph required v1.5.0 and the maximum won. `go mod why -m example.com/c` explains which import path drags C in, and after `go mod tidy` your own `go.mod` will usually carry `example.com/c v1.5.0 // indirect` — the go command recording the resolved minimum so the graph is complete without re-deriving it. ## Determinism Because the inputs are a fixed set of `go.mod` files, the build list is a pure function of the source tree plus the fetched dependency metadata. Two developers, a CI runner and a machine three years from now compute the same answer. There is no lock file for versions because there is nothing left to lock: the resolution has no free variables. (`go.sum` still pins the *content* of each selected version by hash, which is a different guarantee — integrity, not selection.) ## What this rules out - **No backtracking, no SAT solving.** MVS is a graph walk with a max; it always terminates and always gives the same answer. There is no search for a satisfying assignment, because there are no upper bounds to conflict with. - **No "nearest wins".** Depth in the graph is irrelevant; a deeply nested requirement for v1.5.0 beats your own top-level v1.2.0. - **No diamond conflict errors.** Two different requirements for the same path are not a conflict; they are two lower bounds, and the higher one simply applies. ## Checking it `go list -m all` prints the selected build list. `go list -m example.com/c` prints just that module's selected version. If a selected version surprises you, the question to ask is always "which module version in my graph requires that?" — never "why did it upgrade?", because it did not upgrade; it was required.

  • Is it a problem that the module which asked for v1.2.0 is compiled against v1.5.0?
    It is the intended behaviour, resting on the semantic-versioning promise: within a major version, later releases stay backward compatible, and an incompatible change is supposed to arrive under a new module path rather than a new tag. When an author breaks that promise, the breakage lands on whoever's graph pulled the higher minimum in, which is why a build that suddenly fails after adding an unrelated dependency is usually this.
  • Why does the go command sometimes need to write an `// indirect` require into my go.mod?
    So the graph is complete from your file alone. When a module you depend on does not itself record a requirement the build needs — or when the resolved minimum comes from deeper in the graph and must be preserved — `go mod tidy` writes it into the main module's go.mod marked `// indirect`. It documents a resolved minimum you did not import directly.
  • Can MVS fail to find an answer the way a range-based resolver can?
    No. Requirements are only lower bounds, so there is no upper bound for a lower bound to contradict, and no backtracking search. Taking a maximum per module path always yields exactly one answer in one pass. Genuine incompatibilities still exist, but they appear as compile or test failures, not as an unsolvable resolution.

saying these in an interview costs you the question

  • Says the closest or first-declared requirement wins
  • Expects a version conflict error from two different requirements
  • Thinks both versions get built side by side
  • Believes only the main module's go.mod affects selection
  • Calls MVS a constraint solver that searches for a solution