rgoussu@goussu: ~/library/go/modules-and-build
~/library/go/modules-and-build cat go-modules.md

Go modules deep dive

# How Go modules actually work — go.mod directives, semantic import versioning, Minimal Version Selection, the proxy and checksum database.

Conceptsaved 2026-08-09 #go#build#modules#dependencies#tooling

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: GOPROXY defaults to https://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 finer GONOPROXY/GONOSUMDB) bypasses proxy and sumdb for matching paths; corporate proxies (Artifactory, Athens) slot in via GOPROXY.
  • Vendoring survives as an option, not a default: go mod vendor writes a vendor/ 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 first go get through 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; v1 commits to compatibility for that major.
  • A v2+ release means: change the module line to .../v2, tag v2.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