skip to content

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%

answer

  1. the higher version had a reason
  2. remove the reason, not the number
  3. requirers get pulled down too
  4. downgrades never upgrade anything
  5. diff the build list, not just go.mod

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.

solid answer

~50 s

A downgrade is not a local edit — it has to remove the reasons the higher version was selected. Since selection is the maximum of the minimums, any module in the graph whose `go.mod` requires a version above your new target keeps pulling it back up, so the go command downgrades each such module to its highest version that does *not* require the excluded one, and drops the requirement entirely if no such version exists. It will never upgrade some other module to make the downgrade fit. The visible result is that asking for one older module can move several modules down at once; `go get` prints those lines, and diffing `go list -m all` before and after is how I check what really happened. If a module you depend on has no version compatible with your target, MVS alone cannot get you there — you are out of the resolver's hands and into go.mod directives.

code

text · 3 lines
text
$ go get example.com/[email protected]
go: downgraded example.com/b v1.4.0 => v1.1.0
go: downgraded example.com/c v1.5.0 => v1.2.0

go deeper

for a junior

Know that a version is selected as the maximum of what the graph requires, so stating a smaller number on its own does not lower it.

for a middle

Explain the mechanism: modules requiring a higher version of the target get moved down to their highest compatible release, or dropped, and nothing is ever upgraded to compensate.

for a senior

Show the review discipline — diff the whole selected build list, read every moved line, and recognise when a downgrade is simply unreachable through resolution and needs a different decision instead.

for a principal

Weigh the downgrade against alternatives you would have to defend: pinning, forking, or living with the regression. Be clear about who absorbs the risk of several modules silently moving backwards at once.

## Why a downgrade is not symmetric with an upgrade Raising a version is easy: state a higher minimum, and the maximum of the minimums goes up. Lowering one is harder, because the number you want to lower is *derived*. If the graph selected `example.com/c v1.5.0`, some module version stated v1.5.0 as its minimum. Simply writing `require example.com/c v1.2.0` in your own `go.mod` achieves nothing — the maximum is still v1.5.0. So a downgrade must work on the **reasons**: to select a lower version, the requirements that force the higher one have to leave the graph. ## What the go command does When you ask for an older version of a module, the go command applies the downgrade half of minimal version selection: - It finds every module in the graph whose own `go.mod` requires a version of the target **higher than your new target**. - For each of those, it selects **the highest version of that module that does not require the excluded version** — in other words, it walks that dependency backwards until it finds a release whose requirements are compatible with your choice. - If no such version of that module exists, the requirement on it is **dropped from the graph** rather than kept, and you will be told. - It **never upgrades** a module in order to make the downgrade work. Downgrades only move things down or remove them. The practical shape of this is that one requested downgrade can produce a fan-out of several modules moving. That is not the tool being clever behind your back; it is the only way to make the maximum-of-minimums arithmetic produce the number you asked for. ## Reading the result Two checks are worth building into the habit: 1. **The command's own output.** `go get` reports each module it moves, in `path v1.5.0 => v1.3.0` form. Read every line, not just the one you asked for. 2. **A build-list diff.** Capture `go list -m all` before and after and diff them. This catches modules that moved for reasons the command's summary compressed, and it is the artefact to paste into a pull-request description so a reviewer sees the true blast radius. The `go.mod` diff alone is *not* sufficient, because a module can change its selected version without any line in your file changing — the movement happened deeper in the graph. ## When the downgrade cannot be reached Sometimes there is no version of a dependency that tolerates your target. Suppose you must go back to `example.com/c v1.2.0`, but `example.com/b` has required at least `v1.5.0` since its very first release and you cannot build without `b`. Minimal version selection has nothing left to try: every version of `b` re-raises the floor, and dropping `b` breaks the build. At that point resolution has genuinely run out of room, and you are into `go.mod` directives that override the graph rather than participate in it — a different mechanism with different review consequences, and a good moment to ask whether the downgrade is really what you want. ## Why you would downgrade at all Honest reasons show up in production work: a newly required minimum drags in a version with a regression; a transitive bump broke behaviour you depend on; you need to reproduce a build that a customer is running. In each case the value of Go's model is that the *reasons* are explicit — you can point at the module version whose `go.mod` set the floor — and the value of the downgrade algorithm is that it will not quietly leave that reason in place while pretending to honour your request. ## The mental model to keep Think of every requirement in the graph as a floor propping a module up. Upgrading is stacking a taller prop. Downgrading is removing props — and if a prop belongs to a module you still need, the only way to remove it is to take an older version of that module too, or to stop using it. Once you see it that way, the fan-out stops looking surprising and starts looking inevitable.

  • Why does adding a lower require line in your own go.mod not downgrade anything by itself?
    Because selection takes the maximum of the minimums. Your lower number is just another minimum, and it loses to the higher one stated elsewhere in the graph. Only removing or lowering the requirement that stated the higher minimum can change the maximum, which is precisely what the downgrade algorithm goes and does.
  • How do you review a downgrade safely in a pull request?
    Diff `go list -m all` before and after and put that in the description, because the go.mod diff understates the change — modules can move without any line in your file changing. Then confirm the moved modules are ones you can accept at their older versions, and that tests exercise the paths that use them.
  • What if no version of a dependency you need is compatible with the version you are downgrading to?
    Then minimal version selection cannot reach your target: every version of that dependency re-raises the floor, and dropping it breaks the build. Resolution has run out of options, and any remaining route involves go.mod directives that override the graph rather than participate in it — which is a good prompt to re-examine whether the downgrade is the right fix.

Lowering a shelf means shortening every prop underneath it; a prop you cannot shorten has to be taken out of the cupboard entirely.

saying these in an interview costs you the question

  • Thinks writing a lower require line is enough
  • Expects only the named module to change version
  • Believes the go command may upgrade others to compensate
  • Reviews only the go.mod diff after a downgrade
  • Assumes any downgrade target is always reachable