# Go Modules And Versioning

> Guides Go module mechanics and versioning — author and edit go.mod/go.sum through the go command (never by hand), understand that MVS selects the minimum version that satisfies all requirements (not the latest), put /v2 in both the module path and the import path for v2+ (semantic import versioning), keep go.sum committed, run go mod tidy before committing, treat the go directive as a gate on language features and the toolchain line as a request, use replace/exclude/retract correctly, declare build tools with tool directives (1.24) instead of the tools.go hack, and develop multiple local modules together with go.work workspaces. Auto-invokes when writing or editing go.mod/go.sum, adding dependencies, running go get / go mod tidy, doing a major-version /v2 import, using replace/retract/tool directives or go.work, or on "how do I add this dependency" / "why won't this v2 import resolve" requests. The module-lifecycle depth behind the policy root's "work with the toolchain, don't fight it."

- Skill: `ctoth/go-modules-and-versioning` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-modules-and-versioning`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-modules-and-versioning/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ctoth (https://skillmd.com/u/ctoth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ctoth/go-modules-and-versioning

---


# Go Modules and Versioning

> "A module is a collection of packages that are released, versioned, and distributed together."
> — [Go Modules Reference](https://go.dev/ref/mod)

> "Both `go.mod` and `go.sum` should be checked into version control."
> — [Using Go Modules](https://go.dev/blog/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](https://go.dev/blog/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).

```bash
# 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](https://go.dev/blog/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](https://go.dev/blog/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:

```bash
# 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](https://go.dev/ref/mod)). Each `require` line is a *minimum* ("A `require` directive tracks the minimum version of a module that your module depends on" — [Managing Dependencies](https://go.dev/doc/modules/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.

```bash
# 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](https://go.dev/ref/mod)), 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](https://go.dev/ref/mod)). The suffix goes in the **module path in `go.mod`** *and* in **every import path**.

```go
// 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](https://go.dev/doc/modules/version-numbers)). 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](https://go.dev/ref/mod)). 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](https://go.dev/ref/mod)). 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](https://go.dev/doc/toolchain)).

```
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](https://go.dev/doc/modules/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.

```bash
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](https://go.dev/ref/mod)). 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, "`replace` directives only apply in the main module's `go.mod` file and are ignored in other modules" ([Modules Reference](https://go.dev/ref/mod)). A `replace` you 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](https://go.dev/ref/mod)).
- **`retract`** "indicates that a version ... should not be depended upon" ([Modules Reference](https://go.dev/ref/mod)). 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 the `retract` directive.

```
// 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](https://go.dev/ref/mod)).

```bash
# 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](https://go.dev/ref/mod)). 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](https://go.dev/doc/tutorial/workspaces)) — one file instead of scattering (and risking shipping) `replace` lines.

```bash
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](https://go.dev/ref/mod)). 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](https://go.dev/ref/mod)). 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**: the `go` directive (§4) gates which language features compile; that skill is the per-release table that tells you what each `go` line 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` — `govulncheck` reads `go.mod` to scan dependencies; `go vet` and CI wiring live there.
- `go-testing-advanced` — declaring test-only tools (fuzz helpers, mock generators) via the `tool` directive (§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](references/common-mistakes.md)

Source provenance for every claim in this skill:

[references/sources.yaml](references/sources.yaml)

