go.mod and go.sum
Every Go module is defined by two files at its root.
go.modis the declaration: a small, line-oriented, human-editable text file that names the module, states the Go version it targets, and lists its dependency requirements (minimums, not pins).go.sumis the integrity ledger: a machine-maintained list of cryptographic hashes, one set per dependency version, that lets the toolchain prove that the bytes it downloaded are the exact bytes everyone else downloaded. Together they make Go builds reproducible and tamper-evident; both must be committed to version control (go.dev/ref/mod). This note walks everygo.moddirective and thego.sumline format symbol by symbol.
Mental Model
go.mod and go.sum answer two different questions. go.mod answers “what does this module want?” — a description of intent that feeds Minimal Version Selection. go.sum answers “is what I got what was promised?” — a verification layer with no influence on version selection at all.
flowchart LR SRC[Go source: import statements] --> TIDY[go mod tidy] TIDY --> GOMOD["go.mod module path go / toolchain require (minimums) replace / exclude / retract"] TIDY --> GOSUM["go.sum path version h1:hash path version/go.mod h1:hash"] GOMOD --> MVS[Minimal Version Selection] MVS --> BUILDLIST[build list] BUILDLIST --> VERIFY{download hash == go.sum?} GOSUM --> VERIFY VERIFY -- yes --> BUILD[compile] VERIFY -- no --> FAIL[security error: refuse to build]
Diagram: the division of labour. The insight is that go.mod flows into version selection (left/top path) while go.sum flows into verification (right path) — they never mix. A go.sum hash mismatch fails the build as a security event; it never silently picks a different version.
go.mod — Directive by Directive
go.mod is UTF-8 text, one directive per line, with related directives optionally grouped in ( … ) blocks. The toolchain maintains it automatically but it is also designed to be read and edited by hand (go.dev/ref/mod).
module
module example.com/app
Exactly one per file. It declares the module path — the prefix of every package import path in the module, and (for v2+) the path carries the major-version suffix (example.com/app/v2). A // Deprecated: comment directly above the module line marks the whole module as deprecated; tools surface this to consumers.
go
go 1.25.0
Declares the minimum Go language version the module assumes. Since Go 1.21 this is a real requirement, not a hint: a toolchain older than the stated version refuses to build the module, and a 1.21+ toolchain reads it to decide which language features and go command behaviors to enable. The go directive also gates module graph pruning — go 1.17+ turns on the pruned graph. Note (Go 1.26) that go mod init now writes a lower version than the running toolchain on purpose (see Go Modules).
toolchain
toolchain go1.26.0
Added in Go 1.21 (Go 1.21 release notes; Forward Compatibility and Toolchain Management in Go 1.21). Suggests a specific Go toolchain to build with. The reference states that “the suggested Go toolchain’s version cannot be less than the required Go version declared in the go directive,” and that the directive “only has an effect when the module is the main module and the default toolchain’s version is less than the suggested toolchain’s version” (go.dev/ref/mod). If the toolchain version (or, absent it, the go version) is newer than the toolchain actually invoked, the go command will, by default, download and re-exec the newer toolchain — Go’s self-updating mechanism.
require
require (
golang.org/x/text v0.27.0
golang.org/x/sys v0.36.0 // indirect
)
Each line declares a minimum required version of a dependency module. The word minimum is load-bearing: a require x v1.2.0 does not pin v1.2.0; if another module in the graph needs v1.3.0, MVS selects v1.3.0. The // indirect comment marks a requirement that the main module does not import directly — it is there because the pruned-graph rule (go 1.17+) demands that every transitively needed module be listed explicitly. go mod tidy keeps the // indirect annotations accurate.
exclude
exclude golang.org/x/net v0.21.0
Removes a specific module version from consideration. When MVS would otherwise select an excluded version, it behaves as if the requiring module had asked for the next higher version instead. exclude is honored only in the main module’s go.mod — it is ignored when the module is someone else’s dependency, so a library cannot impose exclusions on its consumers (go.dev/ref/mod).
replace
replace example.com/lib v1.4.0 => example.com/fork/lib v1.4.1
replace example.com/lib => ../lib
Redirects a module’s content to a different source — another module path/version, or a local directory (a path beginning with ./ or ../, which must itself contain a go.mod). The first form is version-specific; omitting the left version replaces all versions. replace rewrites the module graph before MVS runs. Like exclude, it is honored only in the main module — it does not propagate to consumers, which is why a replace is fine for local development but cannot be used to ship a patched dependency to downstream users. A bare replace does not by itself add a module to the build; a matching require must also exist.
retract
retract (
v1.0.1 // Published with a critical bug.
[v1.2.0, v1.2.3] // Broken release range.
)
Added in Go 1.16. Declared in the module’s own go.mod, it tells consumers that certain already-published versions should not be used — a single version or a closed interval [lo, hi]. Retracted versions are hidden from go list -m -versions (unless -retracted is passed) and go get warns when a build uses one. Retraction is the only way to “un-publish” in a system where tags are immutable: you cannot delete a bad tag, but you can publish a new version whose go.mod retracts the bad one (the retracting version must sort higher than what it retracts).
tool, godebug, ignore
tool example.com/cmd/x — added in Go 1.24 (go.dev/ref/mod, “Since Go 1.24, a tool directive adds a package as a dependency of the current module”) — records an executable dependency so go tool x works without a separate install; it replaces the old tools.go-with-blank-imports hack for pinning developer tools to a module. godebug key=value — added in Go 1.23 (Go 1.23 release notes) — pins a GODEBUG setting for the main module, so a compatibility knob (e.g. panicnil=1) travels with the code rather than depending on the developer’s environment; the reference defines its effect as “if every main package being compiled contained a source file that listed //go:debug key=value,” and it is an error to name a GODEBUG key that does not exist. ignore ./path — added in Go 1.25 (Go 1.25 release notes) — excludes directories from package-pattern matching (so ./... skips them) while still including them in the module zip, useful for keeping non-Go subtrees such as vendored JavaScript or generated assets out of pattern matches. All three apply in the main module only.
How directives interact with the go version
A non-obvious but important rule: the meaning of go.mod itself depends on the go directive. A go 1.16 module’s graph is unpruned (full transitive closure); a go 1.17+ module’s graph is pruned and must therefore list every transitively needed module explicitly. A go 1.21+ module’s go directive is an enforced minimum, not a hint. Newer directives (toolchain, godebug, tool, ignore) are simply ignored by toolchains older than the release that introduced them — they are forward-compatible by being skippable. This means a go.mod is not a static format but a versioned one: the same file is interpreted differently by different toolchains, and the go directive is the version selector for that interpretation.
A concrete consequence to keep in mind for language features: the go directive does not merely enable — it also gates. The reference is explicit that “the compiler rejects use of language features introduced after the version specified by the go directive. For example, if a module has the directive go 1.12, its packages may not use numeric literals like 1_000_000, which were introduced in Go 1.13” (go.dev/ref/mod). So lowering the go line to claim broad compatibility is not free: it disables newer syntax for the whole module. See Go Release Cycle and Versioning for how this language-version floor relates to the toolchain version and the support window.
go.work and go.work.sum
A workspace adds a third and fourth file at workspace scope: go.work is the workspace analogue of go.mod — it has its own go, toolchain, use (listing member module directories), and replace/godebug directives — and go.work.sum is the analogue of go.sum, holding any extra checksums the workspace build needs beyond what the member modules’ own go.sum files supply. When a go.work is in effect, its used modules all become main modules and its replace directives override the members’. The general guidance is to not commit go.work — it is a developer-local convenience, and a checked-in one can silently override a collaborator’s intended module set.
Uncertain
Verify: the exact introduction releases of the
tool(claimed Go 1.24),godebug(claimed Go 1.23), andignore(claimed Go 1.25) directives. Reason: these are recent additions and the cmd/go reference does not always state the introducing release inline; introduction dates are easy to misremember. To resolve: cross-check each against its release-notes page (go.dev/doc/go1.23,1.24,1.25).
go.sum — the Integrity Ledger
go.sum is not a lock file and does not select versions. It is a list of expected cryptographic hashes; every entry has the form:
golang.org/x/text v0.27.0 h1:ckS3rZ...base64...=
golang.org/x/text v0.27.0/go.mod h1:Lm9P0...base64...=
Each dependency version gets (usually) two lines:
module version h1:hash— the hash of the entire module source zip. Theh1:prefix names the hash algorithm: an SHA-256 of a deterministic, sorted listing of every file’s name and SHA-256, base64-encoded (thegolang.org/x/mod/sumdb/dirhash“h1” scheme). It does not hash the raw zip bytes — it hashes file contents in a canonical order, so it is stable across zip-packing differences.module version/go.mod h1:hash— the hash of just that version’sgo.modfile. This separate entry exists because module-graph loading frequently needs only thego.modof a dependency (to read its requirements) without downloading the full source. Pruning means thego.mod-only hash is often present when the full-zip hash is not.
When the go command downloads a module, it computes these hashes and compares them to go.sum. A mismatch is a hard failure — a potential supply-chain tampering event — and the build stops. New entries are added to go.sum (verified against the checksum database, sum.golang.org, on first download) by go get and go mod tidy; go mod verify re-checks the on-disk module cache against go.sum.
How many entries go.sum holds depends on the go version: go mod tidy records the checksums needed to build with the Go version one below the go directive — a go 1.17 module keeps full-transitive-closure checksums (Go 1.16-compatible), while a go 1.18+ module keeps only pruned-graph checksums. The -compat flag overrides this (go.dev/ref/mod).
A complete, realistic go.mod
Putting the directives together, a production go.mod for a go 1.25 application looks like this:
module github.com/acme/billing
go 1.25.0
toolchain go1.26.0
require (
github.com/jackc/pgx/v5 v5.6.0
golang.org/x/sync v0.10.0
)
require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/text v0.21.0 // indirect
)
replace github.com/acme/internal-logging => ../internal-logging
exclude golang.org/x/net v0.30.0
The two require blocks are conventionally split — direct dependencies first, // indirect ones second — though the grouping is cosmetic; go mod tidy produces it. The replace points a dependency at a sibling checkout for local development of two modules together (a go.work file would be the cleaner choice — see Go Workspaces — precisely because a committed replace like this breaks for anyone who clones without the sibling directory). The exclude pins out one known-bad x/net patch. The toolchain line, newer than the go line, makes the go command fetch Go 1.26 if an older one is invoked.
How the toolchain reads go.mod during a build
When go build runs, it reads the main module’s go.mod first (lazy loading — see Go Modules). It uses the go directive to decide whether the graph is pruned, the require minimums as MVS inputs, replace/exclude to rewrite the graph, and toolchain to decide whether to re-exec a newer Go. It then computes the build list, and for each selected module version checks the downloaded content against go.sum. Only after every hash matches does compilation begin. A missing go.sum entry for a needed module is itself an error (“missing go.sum entry; to add it: go mod download …”) — the toolchain will not build content it cannot verify.
Failure Modes and Common Misunderstandings
“go.sum is the lock file.” No. go.sum verifies content; it never picks versions. Two builds with identical go.mod files but different go.sum files select the same versions — one of them just fails verification.
Deleting go.sum to “fix” a checksum error. This discards the integrity baseline and lets a tampered or simply different module slip in. The correct response to a checksum mismatch is to investigate why the bytes changed (a re-tagged upstream release, a compromised proxy) before regenerating.
Hand-editing require and breaking // indirect. In go 1.17+ modules the pruned graph depends on indirect requirements being listed. Removing an // indirect line by hand can produce “missing go.sum entry” or resolution errors; go mod tidy is the repair.
Expecting replace to affect consumers. replace and exclude are main-module-only. Shipping a library with a replace directive has no effect on anyone who depends on that library — a frequent surprise when trying to “fix” a transitive dependency for downstream users.
Stale go.mod after refactoring. Removing the last import of a dependency does not remove its require line; only go mod tidy does. Conversely, go build adds requirements but a -mod=readonly build (the default since Go 1.16) refuses to and reports the needed change instead.
Alternatives and When to Choose Them
There is no real “alternative” to go.mod/go.sum for a module — they are the module. The decisions are around them: a vendor directory supplements go.sum for fully offline builds (the vendored copies plus vendor/modules.txt replace network access); a go.work file supplements go.mod for editing several modules at once without committing replace lines. The go.work/go.work.sum pair mirrors go.mod/go.sum at workspace scope.
The h1: Hash Scheme in Detail
Because go.sum is the supply-chain integrity boundary, it is worth understanding exactly what an h1: hash covers. The scheme (the “h1” dirhash algorithm in golang.org/x/mod/sumdb/dirhash) does not hash the raw bytes of the downloaded zip file — that would be fragile, since two correct zips of the same source can differ in compression, file ordering, or metadata. Instead it computes a canonical hash: for every file in the module, it takes SHA-256(file contents), formats a line hexhash filename, sorts those lines lexicographically by filename, concatenates them, and takes SHA-256 of the whole — then base64-encodes the result with the h1: prefix naming the scheme. The /go.mod hash variant applies the same construction to a one-file set containing only go.mod.
This canonical construction is what makes go.sum portable: a module re-zipped on a different operating system, or fetched through a different proxy, still produces the identical h1: hash as long as the file contents are unchanged. A mismatch therefore unambiguously means the source itself differs — a re-tagged release, a compromised mirror, or genuine tampering — never a benign packaging difference. The h1: prefix is a version marker: if the hash construction ever needs to change, a new scheme (h2:) can coexist without invalidating old entries.
Production Notes
The operational rule is simple and near-universal: commit both files, regenerate with go mod tidy, review go.sum diffs. A pull request that changes go.sum is changing the integrity boundary of the entire dependency graph and deserves the same scrutiny as a code change — an unexpected hash change can be a re-tagged upstream release or a supply-chain compromise. CI pipelines commonly run go mod verify and build with -mod=readonly (default since Go 1.16) so that any drift between go.mod, go.sum, and the source fails the build instead of being silently patched. For private dependencies, GOPRIVATE/GONOSUMCHECK-style settings tell the toolchain to skip the public checksum database for internal module paths — without that, the first fetch of a private module fails trying to reach sum.golang.org.
See Also
- Go Modules — the module system overview and the
go modcommand family - Minimal Version Selection — how the
requireminimums become a build list - Module Proxy and Checksum Database — where
go.sumhashes are independently verified - Go Toolchain Management — the
toolchaindirective and self-updating Go - Vendoring in Go —
vendor/andvendor/modules.txt - Go Workspaces —
go.work, the workspace-scope analogue - Go Release Cycle and Versioning — the
godirective as language-version floor vs. the toolchain version and support window - Go Internals MOC — parent map (§15)