{} The Go Reference

Tooling · Stdlib · Intermediate

Modules & Dependencies

Dependency management in depth — go.mod and go.sum, go get / mod tidy, semantic versioning, minimal version selection, replace directives, workspaces, and vendoring.

Tooling Intermediate ⏱ 7 min read Complete

📦 Analogy

If packages are rooms in your building, a module is the building’s mailing address plus a signed manifest of every other building it leans on. go.mod is that manifest — who you depend on and which version — and go.sum is the tamper-proof seal. This page is about managing that supply chain, not about organizing your own rooms.

go.mod and go.sum: the manifest and the seal

go mod init creates a module. The module path is the import prefix for every package inside it:

$ go mod init example.com/app
go: creating new go.mod: module example.com/app
module example.com/app

go 1.25

require (
	github.com/google/uuid v1.6.0      // direct dependency
	golang.org/x/text v0.14.0 // indirect // pulled in transitively
)

The module line sets the import prefix, go declares the language version, and each require pins a dependency. An import path is the module path plus the subdirectory — so example.com/app/store is the store package within this module, and github.com/google/uuid resolves to that module’s root package.

go.sum is not the lock file you may know from other ecosystems — go.mod already pins exact versions. Instead go.sum records a cryptographic checksum for every module version (and its go.mod), so a download either matches what you verified or the build fails:

github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

The daily commands

Four commands cover almost everything:

$ go get github.com/google/uuid@v1.6.0   # add or pin a version
$ go get github.com/google/uuid@latest   # move to the newest release
$ go get -u ./...                         # upgrade deps to newer minors/patches

$ go mod tidy        # add missing requires, drop unused ones, fix go.sum
$ go mod download    # pre-fetch modules into the local cache (CI warm-up)

Versions use semantic versioningvMAJOR.MINOR.PATCH. A bump in MAJOR signals a breaking change, and Go encodes that in the import path itself: v2+ modules append the major version, e.g. github.com/foo/bar/v2. You can also go get a branch or commit, and Go synthesizes a pseudo-version like v0.0.0-20240115093000-abcdef123456.

graph TD
APP["your module<br/>requires lib v1.2.0"] --> SEL["Minimal Version Selection"]
DEP["dependency X<br/>requires lib v1.4.0"] --> SEL
DEP2["dependency Y<br/>requires lib v1.1.0"] --> SEL
SEL --> OUT["build uses lib v1.4.0<br/>highest *required*, never newer"]

Minimal Version Selection (MVS) is why Go needs no range-solving resolver: for each module it picks the highest version anyone in the graph requires — and nothing newer unless you ask. Builds are reproducible by construction.

replace, workspaces, and vendoring

A replace directive redirects a module path — to a local checkout or a fork — without changing any imports:

// Test an unreleased fix from a local clone:
replace github.com/google/uuid => ../uuid

// Or point at your fork at a specific commit:
replace github.com/google/uuid => github.com/you/uuid v1.6.1-patch

For developing several modules together, prefer a workspace over committing replace lines. go work overlays multiple modules locally without touching their go.mod:

$ go work init ./app ./lib     # creates go.work spanning both modules
$ go work use ./tool           # add another module to the workspace
go 1.25

use (
	./app
	./lib
)

Now app resolves lib from your local tree. Crucially, go.work is local — don’t commit it; teammates and CI build against the published versions in go.mod.

Vendoring copies all dependencies into a vendor/ directory committed to your repo, for hermetic, network-free builds:

$ go mod vendor      # populate ./vendor from go.mod
$ go build -mod=vendor ./...   # build using the vendored copies

🐹 Commit go.sum; never edit it by hand

go.sum is security infrastructure, not noise — commit it so every build verifies the same bytes you did. Don’t hand-edit it; let go mod tidy and go get maintain it. If a checksum mismatch appears, it means a module’s contents changed for that version: investigate before you blow it away with go clean -modcache. And run go mod tidy before every PR so go.mod/go.sum reflect exactly what the code imports.

See also

Next: making that code fast — measure first with Profiling with pprof.

Check your understanding

Score: 0 / 5

1. What does `go mod tidy` do?

`go mod tidy` reconciles go.mod/go.sum with what your source actually imports: it adds missing direct/indirect requirements and prunes ones no longer referenced. It does not upgrade pinned versions — that's `go get -u`.

2. How does Go pick which version of a dependency to use when several are requested?

Go uses Minimal Version Selection: from all the versions required across the dependency graph, it picks the highest *required* one for each module — never silently newer. This makes builds reproducible without a lock file resolver doing range solving.

3. What is a `replace` directive for?

`replace example.com/lib => ../lib` points an import at a local path (or a fork) instead of the published module — invaluable for testing an unreleased fix. For multi-module local dev, `go work` is now usually cleaner than committing replace lines.

4. How is go.sum different from a lock file in other ecosystems?

In npm/pip, the lock file resolves a version range to concrete versions. In Go, go.mod + Minimal Version Selection already make the version set deterministic. go.sum is purely integrity: a checksum per module (and its go.mod) so a tampered or changed download fails the build.

5. How does Go handle a v2+ (breaking) major version of a module?

Semantic Import Versioning puts the major version in the path for v2 and up (.../v2, .../v3). Because the paths differ, a build can depend on both v1 and v2 of a library at once, and a major bump never breaks code that hasn't migrated.

Comments

Sign in with GitHub to join the discussion.