Go Modules and Versioning
"A module is a collection of packages that are released, versioned, and distributed together." — Go Modules Reference
"Both
go.modandgo.sumshould be checked into version control." — Using Go Modules
A module is a tree of packages with a go.mod at its root: a module path, a go directive, a require list, and optionally toolchain, replace, exclude, retract, and tool directives. The go command — not your text editor — is the source of truth for both go.mod and go.sum. This skill owns the versioning lifecycle; the on-disk package tree (internal/, cmd/) belongs to go-project-layout, and govulncheck/go vet belong to go-tooling-and-static-analysis.
The anatomy of a complete go.mod, every directive of which this skill covers:
module github.com/example/app // §0 the module path: identity AND import prefix
go 1.24.0 // §4 minimum Go; gates language features
toolchain go1.26.0 // §4 preferred build toolchain (>= the go line)
require (
github.com/example/dep v1.4.0 // §2 a MINIMUM version, not a pin to "latest"
golang.org/x/tools v0.21.0 // (indirect deps are marked // indirect)
)
replace github.com/example/dep => ../dep // §6 main-module-only; dev hack
exclude golang.org/x/tools v0.20.0 // §6 refuse one bad version
retract v1.3.0 // §6 pull back our own bad release
tool golang.org/x/tools/cmd/stringer // §7 a build/test tool dependency
0. The Module Path Is the Identity
The module path "is also the import path used for the root directory" (Using Go Modules) — it is both the module's published identity and the prefix every consumer imports. Set it to the location it will actually be fetched from (the repo URL, e.g. github.com/you/proj), because the toolchain resolves imports by that path. A made-up path like myapp works only until someone else needs to import it, then nothing resolves. For v2+, the path also carries the /vN suffix (§3).
# RIGHT — the path matches where the code lives, so imports resolve for everyone
go mod init github.com/example/app
1. go.mod and go.sum: Let the Tool Write Them
go.mod "defines the module's module path ... and its dependency requirements" (Using Go Modules). go.sum "contain[s] the expected cryptographic hashes of the content of specific module versions" and exists "to ensure the modules your project depends on do not change unexpectedly, whether for malicious, accidental, or other reasons" (Using Go Modules) — it is your supply-chain integrity check.
Commit go.sum. Never hand-edit it, and never hand-edit go.mod for a dependency change. Use go get / go mod tidy, which keep the two files consistent and add the right checksums. For mechanical edits (bumping the go line, adding a replace) go mod edit is the scripted, validated path:
# WRONG — hand-typing a require line; go.sum now lacks the checksum, build fails
echo 'require rsc.io/quote v1.5.2' >> go.mod
# RIGHT — the tool edits go.mod AND records the checksum in go.sum
go get rsc.io/quote@v1.5.2
2. MVS: Go Picks the Minimum, Not the Latest
Go builds with Minimal Version Selection: "the highest required versions comprise the build list: they are the minimum versions that satisfy all requirements" (Modules Reference). Each require line is a minimum ("A require directive tracks the minimum version of a module that your module depends on" — Managing Dependencies); the build uses the largest of those minimums across the whole graph, and nothing newer. There is no SAT solver and no "grab the latest" — upgrades are always explicit.
# Adding a dep pins its MINIMUM; a transitive bump won't silently float you forward
go get rsc.io/quote # records the version it resolves now
go get rsc.io/quote@latest # explicit: opt in to the newest tagged version
go get rsc.io/quote@v1.5.2 # explicit: a specific version
This is why "it didn't auto-upgrade" is the expected behavior, not a bug. Reproducible builds are the point.
3. Semantic Import Versioning: v2+ Needs /vN in Both Paths
This is the single most common module mistake. For v0 and v1 the module path and import path are bare. Starting at v2, "module paths must have a major version suffix like /v2 that matches the major version" (Modules Reference), and because "packages in a new major version of a module are not backwards compatible ... starting with v2, packages need new import paths" (Modules Reference). The suffix goes in the module path in go.mod and in every import path.
// WRONG — importing a v2+ module without /v2: "no required module provides package"
import "github.com/example/mod" // resolves to v1.x only, never v2
// RIGHT — the major version is part of the import path
import "github.com/example/mod/v2"
// In the dependency's own go.mod at v2.0.0:
module github.com/example/mod/v2
v0 "makes no stability or backward compatibility guarantees"; "A v1 or above version number signals that the module is stable" (Module Version Numbering). A v2 release "is a new module with a separate history" — so two major versions can even coexist in one build.
4. The go Directive Gates Language Features; toolchain Requests a Version
The go line is not cosmetic. "The go directive sets the minimum version of Go required to use this module" and "the compiler rejects use of language features introduced after the version specified by the go directive" (Modules Reference). So go 1.21 forbids range-over-func; go 1.22 is what turns on per-iteration loop variables. Bumping the line is a real decision — it changes what compiles and raises the floor for your consumers. Which idiom a given go line permits is owned by go-version-feature-map; consult it before raising the line.
The separate toolchain line "declares a suggested Go toolchain" whose version "cannot be less than the required Go version declared in the go directive" (Modules Reference). The go line is the minimum required; the toolchain line is a preferred newer build, selected via GOTOOLCHAIN (default auto, which switches automatically — Go Toolchains).
module github.com/example/app
go 1.24.0 // minimum: rejects callers on older Go; gates language features
toolchain go1.26.0 // preferred build toolchain (>= the go line)
5. Keeping Dependencies Honest: go mod tidy
go mod tidy "edits your go.mod file to add modules that are necessary but missing. It also removes unused modules that don't provide any relevant packages" (Managing Dependencies). Run it before every commit that touches imports — a stale require list (a removed import still listed, or a new one missing) is a routine review failure and breaks reproducibility for others.
go get example.com/dep@v1.4.0 # add or upgrade a specific dependency
go mod tidy # reconcile go.mod/go.sum with actual imports
go mod download # pre-populate the module cache (CI warm-up)
go mod verify # check cached modules against go.sum
go get is for managing requirements; to install an executable use go install: "the go install command builds and installs the packages named by the paths" into GOBIN (Modules Reference). Since Go 1.16 go install pkg@version is the way to install a tool globally; go get no longer installs commands.
6. replace, exclude, retract
replace"replaces the contents of a specific version of a module ... with contents found elsewhere" — a local fork or path. Critically, "replacedirectives only apply in the main module'sgo.modfile and are ignored in other modules" (Modules Reference). Areplaceyou ship to consumers does nothing for them and signals an unfinished local hack. It is a dev-only tool; remove it before publishing (and prefer a workspace, §8).exclude"prevents a module version from being loaded" — rare; for a known-bad transitive version (Modules Reference).retract"indicates that a version ... should not be depended upon" (Modules Reference). You publish it from the bad module itself to pull back a release you already tagged: "users will not upgrade to it automatically." Retracting needs a new (higher) version that carries theretractdirective.
// In the module that shipped a broken v1.3.0:
retract v1.3.0 // panics on empty input; use v1.3.1
7. tool Directives (Go 1.24): Retire the tools.go Hack
Before 1.24, build/test tools (stringer, mockgen, a linter) were pinned with a tools.go file full of blank imports plus a build tag — a workaround. Since Go 1.24, "a tool directive adds a package as a dependency of the current module ... [and] makes it available to run with go tool" (Modules Reference).
# WRONG (pre-1.24 hack) — a fake file just to keep the dep in go.mod:
# //go:build tools
# import _ "golang.org/x/tools/cmd/stringer"
# RIGHT (1.24+) — declare it, then run it
go get -tool golang.org/x/tools/cmd/stringer
go tool stringer -type=Pill
This adds a tool golang.org/x/tools/cmd/stringer line to go.mod. Test-only tools belong here too — see go-testing-advanced.
8. Workspaces (go.work): Multiple Local Modules at Once
A workspace is "a collection of modules on disk that are used as the main modules" (Modules Reference). When you are editing two modules together (a library and its consumer), go.work "can be used instead of adding replace directives to work across multiple modules" (Workspaces tutorial) — one file instead of scattering (and risking shipping) replace lines.
go work init ./hello ./world # creates go.work listing both modules
go work use ./third # add another module to the workspace
Do not commit go.work. "It is generally inadvisable to commit go.work files into version control systems" because a checked-in file "might override a developer's own go.work" and "may cause a continuous integration (CI) system to ... test the wrong versions" (Modules Reference). It is local dev configuration — add it to .gitignore.
9. Don't Vendor by Default
go mod vendor "constructs a directory named vendor ... containing copies of all packages needed to build" (Modules Reference). The module cache plus go.sum already give reproducibility and integrity, so vendoring is no longer the default — reach for it only when you have a concrete need (air-gapped builds, an audit requirement that all source live in-repo). Adding a vendor/ tree "just in case" is the over-built move the policy root warns against.
10. Routing to Related Skills
go-idiomatic-discipline— the policy root; modules is "work with the toolchain, don't fight it" applied to the dependency lifecycle.go-version-feature-map— tight link: thegodirective (§4) gates which language features compile; that skill is the per-release table that tells you what eachgoline unlocks.go-project-layout— owns the on-disk package structure (internal/,cmd/<name>/); modules owns versioning. Cross-link, no overlap.go-tooling-and-static-analysis—govulncheckreadsgo.modto scan dependencies;go vetand CI wiring live there.go-testing-advanced— declaring test-only tools (fuzz helpers, mock generators) via thetooldirective (§7).
11. Reference Files
High-frequency module and versioning mistakes in LLM-generated Go, each with wrong/right code and citations:
references/common-mistakes.md
Source provenance for every claim in this skill:
references/sources.yaml