Which go command shows why a module you never required is in your build, and what does it print?
answer
- who dragged this one in
- shortest chain, not the whole graph
- -m switches packages to modules
- requirement edges are not imports
- it can answer: nothing needs it
basics
~20 sgo mod why -m <module> prints the shortest chain of package imports leading from your main module to that module. If nothing in your build imports it, the command reports that the main module does not need it.
solid answer
~50 s`go mod why -m <module>` answers "who dragged this in". It prints a `#` header with the module path and then the shortest path through the *package* import graph: a package in your main module at the top, a package inside the target module at the bottom, and the packages in between. If no package in the build imports it, you get `(main module does not need module ...)` instead of a chain. Drop the `-m` and the argument is a package path rather than a module path. `go mod graph` answers a different question: it dumps every edge of the *module requirement* graph, one `requiring@version required@version` per line, including modules that are required by something but contribute no compiled package to your binary. In review I run `go mod why -m` first, because a real import chain means code I now ship.
code
text · 8 lines$ go mod why -m golang.org/x/text
# golang.org/x/text
example.com/service/internal/report
golang.org/x/text/language
$ go mod why -m example.com/unused
# example.com/unused
(main module does not need module example.com/unused)go deeper
Know the command and how to read its output: go mod why -m names the module, then lists the shortest import chain from your code down to it. Practise on a module you did not add yourself.
Be ready to explain why the module requirement graph and the package import graph are different sizes, and why a module can be required without a single one of its packages being compiled.
Show how you use this in review: run it on the branch, name the file in your repository responsible for the new chain, and separate modules that are now shipped code from ones that are only version constraints.
Own the standard: decide whether every added require line needs a stated importer in the PR description, and whether that check is a human habit or a job that runs on every change.
## Two graphs, and most confusion comes from mixing them Go maintains two distinct graphs, and "why is this dependency here?" has a different answer in each. The **module requirement graph** is the one written down in `go.mod` files. Your `go.mod` has `require` lines; each required module has its own `go.mod` with its own `require` lines; the union of all those edges is the requirement graph. Version selection walks that graph to produce the *build list* — one chosen version per module path. The **package import graph** is what the compiler actually follows. It starts at the packages you build and follows `import` statements, package by package, until it closes. Only packages in this graph get compiled and linked into your binary. These graphs are not the same size. A module can be required — because some dependency's `go.mod` mentions it — while not a single one of its packages is imported by anything you build. That module appears in `go mod graph` output and contributes nothing to your program. ## What `go mod why` does `go mod why` takes package paths. With `-m`, it takes module paths instead and reports, for each one, the shortest path in the package import graph from a package in the main module to *some* package in that module: ``` $ go mod why -m golang.org/x/text # golang.org/x/text example.com/service/internal/report golang.org/x/text/language ``` Read it top-down: `internal/report` (yours) imports `golang.org/x/text/language`, which lives in the `golang.org/x/text` module. One hop, and now you know exactly which of your files to look at. When nothing imports it: ``` $ go mod why -m golang.org/x/text # golang.org/x/text (main module does not need module golang.org/x/text) ``` That is not an error and the exit status stays zero. It means the module is in the requirement graph — probably because some other dependency declares it — but no package of it is compiled into your build. `go mod why -m all` runs the same report over every module in the build list, which is the fastest way to see the whole provenance picture at once. The `-vendor` flag makes it ignore tests of dependencies, matching what a vendored build would need. ## Why this is the first thing to run when reviewing a new import A pull request that adds one line — `import "example.com/lib"` — routinely adds several `require` lines to `go.mod`. The reviewer's question is not "are these lines legitimate" but "which of them are now *code in the binary*, and which are just graph bookkeeping". `go mod why -m` separates those two populations in seconds, and it names the file in your own repository that is responsible, so the conversation on the PR is about a concrete import site rather than about a diff in `go.mod`. The package-level counterpart is `go list -deps ./...`, which enumerates every package the build compiles, standard library included. Between the two you can say precisely what a change added: `go mod why` gives provenance per module, `go list -deps` gives the inventory per package. ## What `go mod graph` is for instead ``` $ go mod graph example.com/service golang.org/x/[email protected] example.com/service example.com/[email protected] example.com/[email protected] golang.org/x/[email protected] ``` Each line is one requirement edge: the module on the left declares a requirement on the module on the right. The main module appears with no version. This is the right tool when you want to know *who declared* a requirement, especially when two dependencies declare different versions of the same module. It is the wrong tool for "is this code in my binary", because a requirement edge is not an import. ## Common mistakes - Reading the `require` block as an inventory of shipped code. It is a set of version constraints, not an import list. - Assuming `go mod graph` output is an import chain. It is requirement edges, and it usually contains modules that contribute nothing to the build. - Forgetting that test files count. A module can be needed only by the tests of a package you import; `go mod why` will show that path, and `-vendor` excludes tests of dependencies if that is the view you want.
- When would go mod graph show a module that go mod why -m says the main module does not need?Whenever some dependency's go.mod declares a requirement on it but no package of it is ever imported by your build. Requirements exist to constrain version selection; imports decide what gets compiled. That combination is normal and is exactly why the require block is not an inventory of shipped code.
- How do you get provenance for every module at once instead of one at a time?Run `go mod why -m all`. It reports on every module in the build list, printing either the shortest import chain or the "main module does not need" line for each. On a large service the output is long, but grepping it for the "does not need" lines instantly separates graph bookkeeping from code you actually ship.
- The chain go mod why prints ends in a package you have never heard of. What do you do next?Read the intermediate packages in the chain — they name the exact import site in your own code and the exact dependency package that pulls it further. Then open the leaf package's source in the module cache. The chain converts a vague "something needs this" into one file of yours to justify or delete.
saying these in an interview costs you the question
- Treats the require block as the list of code actually shipped
- Reads go mod graph output as an import chain
- Thinks every required module contributes packages to the binary
- Expects go mod why to fail when nothing imports the module
- Cannot name any command that reports dependency provenance