Modules6 min read

Go Module Requirements Are Minimum Versions, Not Lock Entries

Read go.mod requirements through minimal version selection, distinguish go.sum from a lockfile, and inspect the selected build list.

  • modules
  • dependencies
  • toolchains

A require line in go.mod gives a minimum version for one module path. Go’s minimal version selection (MVS) loads the relevant requirement graph and chooses the highest required minimum for each module path; it does not automatically choose the newest release. The resulting build list, not one isolated require line, tells you which versions a command uses.

This article targets the Go 1.27 module reference. Important historical boundaries remain: graph pruning and comprehensive indirect requirements apply to modules declaring Go 1.17 or later, and since Go 1.21 the go directive is an enforced minimum toolchain version.

MVS selects from requirements already in the graph

Suppose the main module requires example.com/a v1.4.0 and example.com/b v1.2.0. Module A requires example.com/log v1.5.0, while module B requires example.com/log v1.7.0. MVS selects example.com/log v1.7.0, the highest minimum in the loaded graph.

If v1.8.0 is published tomorrow, an ordinary build does not start using it merely because it is newer. A command must update a requirement, such as an intentional go get example.com/log@v1.8.0, before that version enters selection.

The Go modules reference defines require as a minimum and describes minimal version selection. This conservative algorithm makes additions to the upstream version list irrelevant to an unchanged module graph.

Inspect the build list, graph, and reason separately

Use the Go command to answer three different questions:

go list -m all
go mod graph
go mod why -m example.com/log

go list -m all prints the selected build list. go mod graph prints requirement edges after replacements are applied. go mod why -m shows a shortest package path from the main module to a package in the named module, or reports that the main module does not need it.

The graph can contain several versions of one module path as vertices even though the build list selects one. If a command seems to “ignore” a direct requirement, look for a higher minimum elsewhere in go mod graph and for active workspace or replacement directives.

MVS works per module path. Semantic import versioning gives incompatible major versions distinct paths—typically /v2, /v3, and so on—so example.com/lib and example.com/lib/v2 can both appear in one build. They are not competing versions of the same module path.

Direct and indirect describe package use

A direct requirement provides a package imported by code in the main module. An indirect requirement does not directly provide such a package; it may be needed to select the right version in the graph or to support pruning and lazy loading.

The // indirect comment does not mean optional, development-only, or unused. Deleting the line can lower a selected version or make the module graph inconsistent. Let go mod tidy maintain it from the packages that belong to the module.

According to the go mod tidy reference, tidy loads packages in the main module, its tools, their recursive imports, and relevant tests. It considers platform-specific files and acts as if all build tags are enabled except ignore. That is why tidy may retain a dependency not used by a build on the developer’s current operating system.

Use go mod tidy -diff when you want CI to report required edits without rewriting go.mod and go.sum.

go.sum authenticates content; it does not select versions

go.sum records cryptographic hashes for downloaded module versions and their go.mod files. It may contain versions that are not in the current build list because commands downloaded them previously or because compatibility checks need their metadata.

Removing a line from go.sum does not instruct MVS to choose another version. If that content is needed again, the Go command verifies and restores the checksum. Conversely, a checksum’s presence does not force the version into a build.

Treat go.mod plus the loaded module graph as selection input and go.sum as an integrity record. Commit both. The module-authentication reference explains how the command uses go.sum, the module proxy, and the checksum database.

The go and toolchain lines solve another problem

Dependency module versions and Go toolchain versions use related but distinct selection mechanisms.

module example.com/service

go 1.27.0
toolchain go1.27.1

Since Go 1.21, go 1.27.0 says the module requires at least that Go language and toolchain version. An older toolchain must refuse or switch to a suitable newer toolchain according to its configuration. Before Go 1.21, the line was mainly a compatibility declaration rather than a strictly enforced minimum.

The optional toolchain line suggests a toolchain for work in the main module when the default is older than the suggestion; it cannot suggest a version below the go requirement. The current Go toolchain documentation describes selection and the GOTOOLCHAIN setting. Do not read toolchain as the version used to compile every downstream consumer of your module.

Pruning changes which requirements must be explicit

For modules declaring Go 1.17 or newer, module graph pruning lets the command avoid loading irrelevant transitive requirements from dependencies that also declare Go 1.17 or newer. To make that possible, the main module’s go.mod records a requirement for every module that provides a package needed to build its packages and tests, including indirect ones.

Lazy loading delays reading the wider graph until package resolution needs it. These optimizations do not change MVS’s core rule; they change which graph portions must be loaded and why more // indirect lines can legitimately appear. The module graph pruning section records the Go 1.17 boundary and compatibility behavior.

Workspaces and replacements alter the active view

A go.work file can make several local modules the main modules of one workspace. A replace directive can substitute another module version or a local directory. go mod graph applies replacements, while a local replacement without its own version may make the build impossible to reproduce outside that filesystem.

Before diagnosing CI differences, run go env GOWORK and check whether local and CI commands use the same workspace. Also inspect GOTOOLCHAIN, proxy settings, private-module settings, and vendor mode. If a vendor directory is active for a module declaring Go 1.14 or later, build commands may read packages and version metadata from vendor instead of the module cache; regenerate it with go mod vendor after changing go.mod.

Upgrade intentionally, then verify

Use explicit queries to change requirements:

go get example.com/log@v1.8.0
go get example.com/log@patch
go mod tidy
go test ./...

Version queries such as latest, upgrade, and patch choose candidates according to the module reference. They are update operations, unlike an ordinary build of an unchanged graph. Review both go.mod and go.sum, then inspect go list -m all when the selected version matters.

The durable mental model is: requirements contribute lower bounds, MVS chooses one version per module path from those bounds, go.sum verifies content, and toolchain directives select Go itself. Keeping those roles separate makes dependency changes explainable instead of treating go.mod as a lockfile that it is not.