Module Graph Pruning — Specification¶
Table of Contents¶
- Introduction
- Where Pruning Is Specified
- The
goDirective as the Pruning Switch - The Pruned Module Graph (Per the Reference)
- Self-Contained
go.mod: The Indirect-Requirement Rule - Lazy Module Loading (Per the Reference)
- When the Full Graph Is Loaded
go mod tidy,-go, and-compat(Specified)- Pruning and
vendor/modules.txt - Interaction with MVS and
go.sum - Differences Across Go Versions
- References
Introduction¶
The Go language specification (go.dev/ref/spec) does not specify module graph pruning. Pruning is a property of the module system and tooling, not the language. The authoritative reference is the Go Modules Reference at go.dev/ref/mod, specifically the section "Module graph pruning", supplemented by the Go 1.17 lazy-module-loading design document and the toolchain source.
Sources of truth, in decreasing formality:
- Go Modules Reference — "Module graph pruning" —
go.dev/ref/mod#graph-pruning. - Go 1.17 lazy module loading design document — the proposal that introduced both features.
- Go 1.17 release notes — the user-facing announcement.
- Toolchain source —
cmd/go/internal/modload(graph loading, MVS, tidy).
This file separates "what the reference states" from convention and implementation detail. Where the reference is silent, the toolchain source is the de-facto specification.
Where Pruning Is Specified¶
Pruning is documented officially in:
- Go Modules Reference, section "Module graph pruning" — the normative description of the pruned graph and the self-containment requirement.
- Go Modules Reference, sections on
go.mod,go mod tidy, and thegodirective — how the directive controls pruning and howtidyrecords requirements. - The lazy-loading design document (
proposal/design/36460-lazy-module-loading.md) — the rationale and the algorithm.
A paraphrase of the reference's core statement:
For modules that specify
go 1.17or higher, the module graph includes only the immediate dependencies of othergo 1.17modules, not their full transitive dependencies. The module graph is pruned. So that the pruned graph selects the same versions as the full graph for all imported packages, thego.modfile of ago 1.17(or higher) module must include an indirect requirement for every module that provides a transitively-imported package and is not already an (implied) requirement.
That paraphrase is the substance; the reference expands each clause.
The go Directive as the Pruning Switch¶
The go directive in go.mod determines whether a module's graph is pruned. Per the reference:
- A module whose
godirective is1.17or higher uses the pruned module graph. - A module whose
godirective is1.16or lower uses the full (unpruned) module graph.
Two important scoping rules:
- Pruning is decided by the main module's
godirective. Whether a build's graph is pruned depends on the directive of the module being built, not of its dependencies. - Each dependency's
godirective decides its own self-containment. Within a pruned build, a dependency atgo 1.17+is treated as self-contained (only its direct requirements are added); a dependency atgo ≤ 1.16is expanded to its full transitive requirements, because it did not record enough indirect requirements to be self-contained.
The directive is therefore both a pruning switch (for the main module) and a self-containment marker (for every module in the graph).
The Pruned Module Graph (Per the Reference)¶
The reference defines the pruned module graph as containing:
- The main module.
- Every module that provides a package transitively imported by a package or test of the main module.
- For each
go 1.17+module already in the graph, the modules named in itsrequiredirectives (one level), added to the graph. - The full transitive requirements of any
go ≤ 1.16module in the graph (such modules are not pruned, being non-self-contained).
The reference is explicit on the guarantee:
- The pruned graph is constructed so that, for every package imported by the main module, MVS selects the same version as it would over the full graph.
Equivalently: pruning removes requirement edges that cannot affect the selected version of any imported package — and records, in the main module's go.mod, any version those removed edges would have contributed.
Self-Contained go.mod: The Indirect-Requirement Rule¶
The reference states the rule that makes pruning correct: a go 1.17+ module's go.mod must be self-contained. Specifically, it must contain a (possibly // indirect) require directive for:
- Every module that provides a package or test imported by the main module's packages, and
- Every module whose selected version is determined by a requirement that pruning removed from the loadable graph (so that the version is still recorded somewhere Go reads).
Consequences specified or implied:
- A
go 1.17+go.modtypically lists more// indirectrequirements than ago 1.16one for the same code, because the deep graph is no longer loaded to supply them. go mod tidycomputes and maintains this set. The reference designatestidyas the canonical writer of the requirement set.- The grouping of direct requirements and
// indirectrequirements into separaterequireblocks is a formatting convention produced bytidy; it is not semantically required. A singlerequireblock with// indirectcomments is equivalent.
Lazy Module Loading (Per the Reference)¶
The reference describes lazy module loading as the companion to pruning:
- The
gocommand attempts to load the least of the module graph necessary to perform the requested operation. - For an operation on a
go 1.17+main module whosego.modis already tidy, the main module'sgo.modalone is often sufficient to resolve imported packages — the broader graph is not loaded. - The broader (pruned, then full) graph is loaded lazily, only when an operation requires it.
The reference frames the combined goal: most go commands should run without loading the full module graph, making them fast and resilient to missing or unreachable deep go.mod files.
When the Full Graph Is Loaded¶
The reference and the design document enumerate operations that still require the full module graph:
go mod graph— printing the complete graph.go mod tidy— computing the complete recorded requirement set (and, with-compat, the older regime's graph).go getaffecting modules whose requirements were pruned — the graph deepens to reconcile.- Any build where a
go ≤ 1.16dependency is relevant — that module's full transitive requirements are loaded, because it is not self-contained. - Reconciliation when the pruned graph is insufficient to resolve a version unambiguously.
Outside these, a tidy go 1.17+ module's everyday commands operate on the pruned graph or the main module's go.mod alone.
go mod tidy, -go, and -compat (Specified)¶
The reference documents go mod tidy's flags relevant to pruning:
-go=<version>¶
Sets the go directive of the module to <version> and updates go.mod and go.sum for the module graph regime of that version.
go mod tidy -go=1.16produces ago.modfor the full-graph regime (smaller indirect set).go mod tidy -go=1.17(or higher) produces ago.modfor the pruned regime (larger indirect set).
This flag is the supported mechanism for migrating a module across the pruning boundary.
-compat=<version>¶
Instructs tidy to produce a go.mod and go.sum that also allow the named older Go version to load the module graph and reach the same build list. Specifically:
tidychecks that the build list computed under the current (pruned) regime agrees with the build list the-compatversion would compute under its regime; an inconsistency is reported as an error.tidyretains the additionalgo.sumentries (notablygo.modhashes) that the-compatversion needs to load its graph.
The reference specifies the default: for a module at go 1.17 or higher, go mod tidy defaults -compat to the version immediately preceding the module's go directive (e.g., a go 1.17 module defaults to -compat=1.16).
Pruning and vendor/modules.txt¶
The Go 1.17 changes to vendoring support pruning. Per the reference, vendor/modules.txt records, for each vendored module:
## explicit— the module is a direct dependency of the main module.## go <version>(Go 1.17+) — the dependency module's owngodirective.## explicit; go <version>— both markers combined.
The ## go <version> marker exists so that a build operating in vendor mode (which does not load the module graph) can apply the correct per-module pruning and language semantics using only the vendored metadata. The marker was added in Go 1.17 specifically to support pruning under vendoring.
A vendored go 1.17+ module is therefore consistent with the pruned graph: the package set vendored is what the import graph reaches, and the per-module go markers preserve pruning-relevant metadata. See 03-go-mod-vendor/specification.md.
Interaction with MVS and go.sum¶
MVS¶
Per the reference, Minimal Version Selection is unchanged by pruning. Pruning alters the graph MVS receives, not the algorithm. The construction of the pruned graph guarantees that MVS selects the same version for every imported package as it would over the full graph. See 04-minimal-version-selection-mvs/specification.md.
go.sum¶
Pruning loads fewer go.mod files, so fewer go.mod hashes are strictly required to verify the build. The reference notes:
go mod tidykeepsgo.sumaligned with the modules the pruned graph loads.-compat=<older>retains thego.sumentries the older, full-graph regime needs.- Module content integrity is unaffected: every built module is still verified against
go.sum.
A missing-go.sum-entry error on an older Go after migration indicates an inadequate -compat; the fix is go mod tidy -compat=<that version>.
Differences Across Go Versions¶
The behaviour around pruning has evolved:
- Go 1.16 and earlier — Full (unpruned) module graph. The complete transitive closure of requirements is loaded.
go.modrecords fewer// indirectrequirements. - Go 1.17 — Module graph pruning introduced for modules at
go 1.17+. Lazy module loading introduced.go.modof pruned modules records the larger indirect set.vendor/modules.txtgains the## go <version>marker.go mod tidygains the-compatflag with a default of one version below the directive. - Go 1.18 — Workspaces (
go.work) introduced; pruning remains per-main-module. - Go 1.21 —
toolchaindirective introduced; relevant to reproducibility alongside pruning but does not change pruning rules. Thegodirective's role as the pruning switch is unchanged. - Go 1.21+ — Continued refinements to error messages and tidy behaviour; the pruning model (driven by the
godirective at the1.17boundary) is stable.
The defining rule — go 1.17+ modules use the pruned graph and must keep a self-contained go.mod — has been stable since Go 1.17.
References¶
- Go Modules Reference — authoritative.
- Module graph pruning (reference)
- The
godirective (reference) go mod tidyand-compat(reference)- Go 1.17 lazy module loading design document
- Go 1.17 Release Notes — Module graph pruning
vendor/modules.txtformat (reference)- Minimal Version Selection (reference)
- Source:
cmd/go/internal/modload
In this topic