skip to content

In a Go module, how do you force a transitive dependency up to a fixed version nothing else requires?

level: seniorimportance: should knowfreq 52%

answer

  1. you are already in the graph
  2. the highest minimum wins
  3. name the module, name the version
  4. an indirect require counts the same
  5. prove it with go list -m, not with hope

basics

~20 s

Run go get example.com/[email protected] in the main module. It writes a require line, marked // indirect, and version selection takes the highest required minimum, so the whole build moves to at least that version. Confirm with go list -m.

solid answer

~50 s

Name the module and the version directly: `go get example.com/[email protected]` from the main module. Because version selection resolves each module to the highest minimum anyone requires, a requirement in your own `go.mod` raises the floor for the whole build no matter what your direct dependencies asked for — you do not need the intervening library to release anything. The line is written with `// indirect` if no package of yours imports it, which is normal and correct. Then verify rather than assume: `go list -m example.com/lib` prints the version actually selected, `go mod why -m example.com/lib` shows which of your packages pulls it in at all, and `go mod graph` shows the requirement edges. Run the tests, since you have just moved code that your dependencies — not you — call. The pin survives `go mod tidy` as long as the module still provides an imported package.

code

text · 8 lines
text
$ go get example.com/[email protected]
$ go list -m example.com/lib
example.com/lib v1.4.2
$ go mod why -m example.com/lib
# example.com/lib
example.com/service/store
example.com/api/client
example.com/lib

go deeper

for a junior

Remember that go get takes an explicit version with an @ suffix, and that go list -m tells you which version the build ended up using.

for a middle

Explain why a require line in your own go.mod is enough: selection takes the highest required minimum, and your module is part of that graph.

for a senior

Show the full loop under a deadline — smallest fixing version, one-line diff instead of a fleet-wide upgrade, verification with go list -m and go mod why, tests, and a note in the PR about what was raised ahead of its consumer.

for a principal

Own the follow-through: when a forced bump is acceptable without upstream, who signs off, how the pin is tracked so it is removed once the graph catches up, and how that is reported back to the security owner.

## The situation An advisory names `example.com/lib`, fixed in `v1.4.2`. You do not import it. It is in your build because a library you *do* import requires it, and that library still requires `v1.4.0`. Nothing in the graph asks for the fix, so nothing gives it to you, and there is a date on the ticket. ## Why a single command is enough Module version selection resolves each module to the **highest version anyone in the graph names as a minimum**. Your own `go.mod` is part of that graph — in fact it is the root of it. So a `require example.com/lib v1.4.2` in the main module raises the selected version to at least `v1.4.2` regardless of what any dependency requires, and no dependency can pull it back down. You are not overriding anything or fighting the resolver; you are adding one more requirement, and the maximum moves. The command that writes it: ``` go get example.com/[email protected] ``` Run from the main module. It resolves the version, writes or updates the require line, adds the hashes to `go.sum`, and — because no package of yours imports that module — annotates the line `// indirect`. That comment is correct and should stay: it is a statement about imports, not about strength. An indirect requirement participates in selection exactly like a direct one. Note what this is *not*. `go get -u ./...` would probably also pick the fix up, but it would move every other module in the build list at the same time; on a deadline that is the wrong diff to ask anyone to review. `go get -u=patch ./...` moves less but still touches everything, and it cannot help at all if the fix landed in a new minor. Naming the module and version is the surgical change: one line moves. ## Verify, do not assume Three commands, each answering a different question: - `go list -m example.com/lib` prints the version **actually selected** for the build. This is the one that proves the fix is in. `go list -m all` shows the whole resolved build list if you want the fuller picture. - `go mod why -m example.com/lib` prints the shortest chain of **imports** from a package of yours down to a package in that module — it answers "why is this in my program at all", and a reply of "module ... is not needed" tells you the requirement is only there because you wrote it. - `go mod graph` prints the **requirement** edges, one `from to` pair per line. Tracing from your main module down to the pinned dependency shows you who was asking for the old version, which is what you will paste into the ticket and what tells you which upstream to file an issue against. Then run the tests. You have changed code your dependencies call and you do not — a patched module can still change behaviour, and yours is the build that has to survive it. ## Living with the pin `go mod tidy` will keep the higher version: it adds missing requirements and drops unused ones, but it does not downgrade a requirement that is still needed. What it *will* remove is a requirement whose module no longer provides any imported package — if the library that dragged `example.com/lib` in gets dropped later, the pin goes with it, correctly. The pin is also self-healing in the good direction. Once the intervening library releases a version that requires `v1.4.2` or newer on its own, your explicit line becomes redundant, and the next `go mod tidy` after that upgrade lets the graph's own requirement carry it. Nothing breaks if you leave it either — a floor at a version everyone already exceeds does nothing. What you should not do is treat this as free. You have raised a module ahead of what its consumer was tested against. That is usually fine for a patch release and less obviously fine across a minor. Say so in the PR description, note the advisory that forced it, and prefer the smallest version that carries the fix rather than the newest available — the smaller the step, the smaller the chance that fixing a security problem introduces a behavioural one.

  • Will `go mod tidy` undo the pin by dropping it back to what the graph requires?
    No. `go mod tidy` adds requirements that are missing and removes ones no longer needed, but it does not downgrade a requirement that is still in use. The higher version stays as long as some package you import lives in that module. It only disappears if the dependency that pulled the module in is itself removed.
  • How do you find out which dependency was holding the module at the old version?
    `go mod graph` prints the requirement edges as `module@version dependency@version` pairs; filtering for the module in question shows every requirer and the version each asked for. `go mod why -m` complements it by showing the import chain from your own package down, which tells you which of your imports is responsible for the module being present at all.
  • Why prefer the smallest version carrying the fix over the newest release?
    You are moving a module ahead of what its consumer was tested against, so every extra version between the fix and the newest release is unreviewed behaviour change riding along with a security fix. Taking the minimum that contains the patch keeps the blast radius to the thing the advisory forced, and leaves the wider upgrade as a separate, reviewable change.

saying these in an interview costs you the question

  • Says a transitive dependency cannot be raised without upstream releasing
  • Runs go get -u on everything to fix one module
  • Thinks an // indirect requirement is ignored by version selection
  • Assumes go mod tidy will revert the pin
  • Declares it fixed without checking go list -m or running tests