Go Modules

A module is Go's unit of versioning and distribution: a tree of packages with a go.mod file at its root declaring the module's path and its dependencies. Since Go 1.16, modules are the only supported way to build Go code, and they replaced the old GOPATH workflow entirely.

Go's approach differs from npm or pip in a few deliberate ways. Minimal version selection picks the oldest versions that satisfy every requirement rather than the newest. Major versions are part of the import path. And a checksum database verifies every download. The result is builds that are reproducible without a separate lockfile.

TL;DR

Quick Example

// indirect marks dependencies your code doesn't import directly but that are needed by your dependencies.

Core Concepts

go.mod Directives

go.sum and the Checksum Database

go.sum lists the expected hash of each module version's content and go.mod. On download, Go verifies the hashes, and for public modules also checks them against sum.golang.org, a public transparency log. A compromised repository can't silently change a published version's contents without detection.

Minimal Version Selection

If your module requires pgx v5.5.0 and a dependency requires pgx v5.7.1, Go uses v5.7.1: the minimum version satisfying all requirements. It doesn't pick v5.9.0 just because it exists. Builds only change when some go.mod changes, which is why Go doesn't need a separate lockfile. Upgrades are always explicit (go get -u).

Semantic Import Versioning

Go assumes semantic versioning. Within a major version, newer versions must be backward-compatible. A breaking change requires a new major version with a new import path:

Because v1 and v2 have different paths, a program can even use both at once during a migration. v0.x versions carry no compatibility promise.

Module Proxy

By default, go downloads modules through proxy.golang.org, which caches public module versions. Builds keep working even if the original repository is deleted or renamed, and downloads are fast. Companies often run their own proxy (Athens, Artifactory, GOPROXY-compatible registries).

Workspaces, Private Modules, and Vendoring

Workspaces

When changing a library and an app that uses it at the same time, a go.work file makes the build use your local copies:

Don't commit go.work for repos that others consume as modules. It's a local development convenience. Some monorepos do commit it deliberately.

Private Modules

GOPRIVATE tells Go to fetch matching modules directly from source control, skipping the public proxy and checksum database. Configure Git credentials (a token in .netrc, SSH via url.insteadOf) so go can clone them in CI.

Vendoring

go mod vendor copies all dependencies into a vendor/ directory, which the build then uses automatically. Use it for air-gapped builds or strict auditing requirements; most projects rely on the proxy and module cache instead.

Best Practices

Run go mod tidy Before Committing

It keeps go.mod minimal and accurate. Many CI pipelines run go mod tidy and fail if the files change (git diff --exit-code go.mod go.sum).

Upgrade Deliberately

go get -u ./... upgrades direct and indirect dependencies to their latest minor and patch versions, and go get -u=patch ./... takes patches only. Review changes, run tests, and let Dependabot or Renovate open regular upgrade PRs.

Check for Vulnerabilities

govulncheck ./... reports known vulnerabilities, and only those in code paths your program actually calls, which cuts out most false positives. Run it in CI.

Use replace Temporarily

replace for a local path or a fork is great for debugging or waiting on an upstream fix. Leave a comment and remove it once the fix is released. Note that replace directives only apply in the main module, not when your module is consumed by others.

Common Mistakes

Forgetting the /vN Suffix

When publishing v2+, update the module line in go.mod and every internal import.

Committing go.mod Without go.sum

Without go.sum, other machines can't verify dependencies and builds fail with "missing go.sum entry". Always commit both.

Tagging Releases Incorrectly

Go discovers versions from git tags such as v1.4.2, with the v required. For a module in a subdirectory of a repo, tags need the prefix: tools/v1.0.0. Untagged commits get pseudo-versions (v0.0.0-20260926…-abcdef), which work but are hard to read.

FAQ

Why doesn't Go have a lockfile?

Because minimal version selection is already deterministic. The versions in the go.mod files of your module and its dependencies fully determine the build list, and go.sum pins their content hashes. Nothing changes until someone edits a go.mod.

What does // indirect mean?

The dependency isn't imported directly by your module's packages but is required, usually by a dependency whose own go.mod is incomplete, or to raise a transitive dependency to a newer version. go mod tidy manages these markers automatically.

Is GOPATH still needed?

No. With modules you can put code anywhere. GOPATH survives as the default location for the module cache ($GOPATH/pkg/mod) and for binaries installed with go install ($GOPATH/bin).

How do I install a CLI tool written in Go?

go install example.com/cmd/tool@latest builds and installs it into $GOBIN (default $GOPATH/bin). To pin tools a project depends on, use the tool directive in go.mod (Go 1.24+) and run them with go tool <name>.

Related Topics

References