Go Module Versions and Upgrades
How semantic versioning works for Go modules, why v2 and later need a new import path, pseudo-versions, minimal version selection and safe upgrades.
Go modules use semantic versioning: every release is tagged vMAJOR.MINOR.PATCH, such as v1.6.0. The numbers carry meaning, and the go command relies on that meaning to pick versions and to keep incompatible versions apart.
What the Three Numbers Mean
v1.6.0
| | |
| | patch: bug fixes only
| minor: new features, still backward compatible
major: incompatible changes
- A patch bump,
v1.6.0tov1.6.1, should be a safe upgrade. - A minor bump,
v1.6.0tov1.7.0, adds things but should not break existing code. - A major bump,
v1tov2, may break code that uses the module.
Versions starting with v0, like v0.4.2, make no stability promise. Anything can change between them.
Major Version 2 and Later Change the Import Path
This rule surprises many people. In Go, v2 and later of a module have a different module path, ending in /v2, /v3 and so on:
import "github.com/jackc/pgx/v5"
import "github.com/go-chi/chi/v5"
The idea, called semantic import versioning, is that incompatible versions are treated as different modules. A program can even use v1 and v2 of the same library at once during a migration.
To upgrade a dependency from v4 to v5, you change the import paths in your code and then run go get for the new path:
go get github.com/go-chi/chi/v5@latest
go get github.com/go-chi/chi@v5.0.0 does not work. It fails with invalid version: go.mod has post-v5 module path "github.com/go-chi/chi/v5" at revision v5.0.0.
Publishing v2 of your own module:
To release v2 of your module, change the module line in go.mod to end in /v2, update your own internal imports, and tag v2.0.0. Only tagging v2.0.0 without changing go.mod makes Go treat it as v2.0.0+incompatible or reject it.
Pseudo-Versions
If you depend on a commit that has no tag, Go creates a pseudo-version:
v0.0.0-20260115093045-4b9c3e7f1a2d
It is made of a base version, the commit time in UTC and the first 12 characters of the commit hash. You get one with go get example.com/lib@main or @<commit-hash>. Pseudo-versions are fine for short periods but are harder to read, so ask the maintainers for a tagged release when you can.
How Go Picks a Version: Minimal Version Selection
When several of your dependencies require the same module at different versions, Go picks the highest of the minimum versions they ask for, not the newest release available. If package A needs uuid v1.3.0 and package B needs uuid v1.5.0, your build uses v1.5.0, even if v1.6.0 exists.
This is called minimal version selection. Builds are repeatable: nothing changes until someone edits a go.mod. A new release upstream never silently changes your build.
Upgrading Safely
go list -m -u all # see available updates
go get example.com/lib@latest # upgrade one module
go get -u=patch ./... # take only patch updates for everything
go mod tidy
go test ./...
Upgrade one module at a time when changes are large, run the tests, and read the release notes for minor versions in v0 modules, which can break things.
Retracted versions:
A module author can retract a broken version in their go.mod. go list -m -u all then shows it as (retracted), and go get @latest skips it. If you see a retraction warning for a version you use, upgrade off it.
Understanding versions:
If you can read an import path ending in /v5 and know it is a separate major version, and you know your build only changes when go.mod changes, you understand the Go versioning model.
Summary
Go modules use vMAJOR.MINOR.PATCH tags, where only major versions may break code and v0 promises nothing. Major versions 2 and later live at a different import path ending in /vN, so upgrading means changing imports. Untagged commits get pseudo-versions, minimal version selection makes builds repeatable, and safe upgrades go through go list -m -u, go get, go mod tidy and your tests.