Overview
Go modules are the toolchain's built-in dependency system: a go.mod file names the
module and its requirements, versions are semver git tags, and resolution uses Minimal
Version Selection — an algorithm deliberately simpler than the SAT-style resolvers Maven,
npm, and Cargo use. The design trades resolver power for determinism: the same go.mod
always yields the same build list, so there is no lockfile-vs-manifest split to keep in
sync. Around the core sit two pieces of shared infrastructure — the module proxy and the
checksum database — that make fetching fast and tamper-evident without any central
package registry.
Key points
- Module identity is a VCS path:
module github.com/acme/widget— the import path, the fetch location, and the "registry" name are the same string. No publishing account, no namespace claims beyond controlling the repo. - MVS (Minimal Version Selection): build with the minimum version satisfying every
requirement — i.e. the maximum of the versions explicitly required across the graph,
never anything newer. Deterministic, explainable (
go mod graph,go mod why), and upgrades only happen when you ask (go get -u). - go.sum is not a lockfile: it holds cryptographic checksums of module content for
verification; the versions are already fully determined by
go.mod+ MVS. Commit both. - Semantic import versioning: from v2 onward the major version is part of the import
path (
github.com/acme/widget/v2) — the ecosystem's most controversial rule, but it makes two majors coexist in one build and makes a breaking upgrade loudly visible. - The proxy protocol:
GOPROXYdefaults tohttps://proxy.golang.org,direct; the proxy serves an immutable cache of every public module version via a simple HTTP GET API, so builds don't depend on VCS hosts being up or fast. - The checksum database (
sum.golang.org): an append-only transparency log of module hashes; the toolchain verifies first-time downloads against it, so the proxy (or a hijacked repo tag) cannot silently serve altered code. - Private code:
GOPRIVATE=*.corp.example.com(or finerGONOPROXY/GONOSUMDB) bypasses proxy and sumdb for matching paths; corporate proxies (Artifactory, Athens) slot in viaGOPROXY. - Vendoring survives as an option, not a default:
go mod vendorwrites avendor/tree, used automatically when present — chosen for air-gapped builds, review-the-diff policies, or hermetic CI.
Details
go.mod directives
| Directive | Meaning |
|---|---|
module |
The module path — import-path prefix for all packages within |
go |
Language/toolchain semantics version the module is written for (also gates behaviour changes, e.g. loop-variable scoping) |
toolchain |
Minimum Go toolchain to build with; newer go commands auto-switch to it (Go 1.21+) |
require |
Direct (and pinned indirect, marked // indirect) dependency versions |
replace |
Redirect a module path/version to another path or a local directory — local-only, ignored in consumers of your module |
exclude |
Refuse a specific version of a dependency in this module's resolution |
retract |
Author-side: mark your own published versions as broken so go get avoids and warns about them |
MVS vs the rest of the world
Maven resolves conflicts by "nearest wins" plus dependencyManagement overrides; npm and
Cargo run constraint solvers over ranges and freeze the answer in a lockfile. MVS refuses
the problem: requirements are exact minimums, never ranges, so resolution is a max over
declared versions — no solver, no backtracking, no separate lock artifact. Costs: you get
bugfix upgrades only explicitly (go get -u ./...), and a library declaring a too-new
minimum forces it on all consumers. See
Maven deep dive for the contrasting model.
Publishing and major versions
- Publishing is
git tag v1.4.2 && git push --tags— no upload step. The firstgo getthrough the proxy caches it forever (immutable: never re-tag), and pkg.go.dev indexes docs automatically from the proxy's copy. - Pre-v1 (
v0.x) promises nothing;v1commits to compatibility for that major. - A v2+ release means: change the
moduleline to.../v2, tagv2.0.0, and consumers rewrite imports. Common practice keeps v2 on the main branch with v1 on a maintenance branch;gorelease(x/exp) helps check API compatibility before tagging. - In practice major bumps are rare and painful (every import site changes), which pushes the ecosystem — like the stdlib — towards additive evolution instead.
Examples
module github.com/acme/widget/v2
go 1.22
toolchain go1.22.5
require (
github.com/jackc/pgx/v5 v5.6.0
golang.org/x/sync v0.7.0
)
require github.com/stretchr/testify v1.9.0 // indirect
replace github.com/acme/internal-fork => ../internal-fork
retract v2.0.1 // published with a data-corruption bug
Related
- Modules & the Go build ecosystem — the parent summary note.
- Building & releasing — what happens after resolution: reproducible builds and release automation.
- go mod — the subcommand surface: tidy, graph, why, vendor.
- Maven deep dive — the resolver-and-central-repository model Go's MVS-and-proxy design deliberately rejects.