Module Versioning — Specification¶
Table of Contents¶
- Scope
- Version String Grammar
- Canonical Form
- Version Comparison
- Pseudo-Version Grammar
- Major Version Suffix Rule
+incompatibleRulego.modDirectives Related to Versioning- Minimum Version Selection Definition
- Authoritative References
Scope¶
This file paraphrases the normative rules that govern Go module versions: how a version string is shaped, how two versions compare, how the toolchain assembles a pseudo-version, and what relationship between go.mod and the module path the toolchain enforces. It is a reference, not a tutorial; for tutorial material see junior.md through professional.md.
The authoritative sources are the Go modules reference at go.dev/ref/mod and the upstream semver.org 2.0.0 specification. Where the two differ, go.dev/ref/mod governs Go-specific behaviour.
Version String Grammar¶
A canonical version in Go has the shape:
version := "v" major "." minor "." patch [ "-" prerelease ] [ "+" build ]
major := decimal ; no leading zeros except "0"
minor := decimal
patch := decimal
prerelease := identifier ( "." identifier )*
build := identifier ( "." identifier )*
identifier := alphanumeric ; numeric identifiers must not have leading zeros
alphanumeric := [0-9A-Za-z-]+ ; with at least one non-digit if numeric leading-zero would otherwise apply
decimal := "0" | [1-9] [0-9]*
Rules in prose:
- Mandatory leading lowercase
v. - Three numeric components separated by
.. - No leading zeros in any numeric component (except a literal
0). - Optional
-prereleaseblock. Identifiers separated by.. Each identifier is alphanumeric (with-allowed) or pure numeric (no leading zeros). - Optional
+buildmetadata. Same identifier shape, but ignored for ordering. - No whitespace anywhere.
Examples of valid versions¶
v1.0.0v0.1.0v2.5.7v1.0.0-alphav1.0.0-alpha.1v1.0.0-0.3.7v1.0.0-x.7.z.92v1.0.0+20130313144700v1.0.0-beta+exp.sha.5114f85v1.5.0+incompatible(special, see below)
Examples of invalid versions¶
1.0.0— missingvV1.0.0— capitalVv01.0.0— leading zerov1.0— only two numeric componentsv1.0.0.0— four numeric componentsv1.0.0-— trailing-with no identifierv1.0.0-alpha..1— empty identifierv 1.0.0— whitespace
Canonical Form¶
Every version has exactly one canonical form. The toolchain rejects non-canonical forms in go.mod. Canonicalisation rules:
- All numeric components present (
v1andv1.0are not canonical;v1.0.0is). - Lowercase
v. - No leading zeros.
- No redundant
+buildmetadata when ordering is unaffected — but Go preserves whatever was given.
The function golang.org/x/mod/semver.Canonical(v) returns the canonical form of an input string (or empty if invalid).
Version Comparison¶
Defined by golang.org/x/mod/semver.Compare(v, w), which returns -1, 0, or +1. The algorithm:
- Strip the leading
v. - Compare
MAJOR,MINOR,PATCHnumerically in order. If any differs, return that comparison. - If all three are equal:
- If neither has a pre-release, return equal.
- If exactly one has a pre-release, the one with a pre-release is less.
- Otherwise compare pre-release components left-to-right:
- If both are numeric, compare numerically.
- If both are alphanumeric, compare lexicographically.
- If types differ, numeric is less than alphanumeric.
- If one runs out of components first, the shorter one is less.
+buildmetadata never affects ordering.
Worked comparisons¶
v | w | Result |
|---|---|---|
v1.0.0 | v1.0.0 | 0 |
v1.0.0 | v1.0.1 | -1 |
v1.0.0 | v0.999.999 | +1 |
v1.0.0-alpha | v1.0.0 | -1 |
v1.0.0-alpha | v1.0.0-alpha.1 | -1 |
v1.0.0-alpha.1 | v1.0.0-alpha.10 | -1 (numeric) |
v1.0.0-alpha.10 | v1.0.0-beta | -1 (alphanumeric alpha < beta) |
v1.0.0-1 | v1.0.0-alpha | -1 (numeric < alphanumeric) |
v1.0.0+a | v1.0.0+b | 0 (build metadata ignored) |
v2.0.0+incompatible | v2.0.0 | 0 |
Pseudo-Version Grammar¶
A pseudo-version is generated by cmd/go for any commit that is not the target of a semver tag. The grammar:
Where base depends on context:
- If no semver tag is reachable from the commit's history:
0.0.0. - If a tag
vX.Y.Zis reachable and the commit comes after it on the same major:X.Y.(Z+1)-0. - If a tag
vX.Y.Z-preis reachable:X.Y.Z-pre.0.
timestamp is the commit time in UTC formatted as YYYYMMDDHHMMSS.
hash is the first 12 lowercase hex digits of the commit hash.
Pseudo-version examples¶
| Base tag | Pseudo-version |
|---|---|
| (none) | v0.0.0-20240612103515-abc123def456 |
v1.5.0 | v1.5.1-0.20240612103515-abc123def456 |
v1.5.0-rc.1 | v1.5.0-rc.1.0.20240612103515-abc123def456 |
v2.0.0 (with /v2 path) | v2.0.1-0.20240612103515-abc123def456 |
Ordering with pseudo-versions¶
Pseudo-versions interleave with normal versions according to the same comparison rules:
The -0. pre-release marker keeps pseudo-versions strictly between the preceding tag and the next patch.
When the toolchain emits a pseudo-version¶
When the user references a Git ref that is not a canonical tag:
- A commit hash (
@abcdef1). - A branch name (
@main,@master,@HEAD). - A non-semver tag (
@release-2024). - An empty version (
@) — equivalent to@latestbut for an untagged repo.
Major Version Suffix Rule¶
For any module at major version N >= 2:
- The module path declared by the
moduledirective ingo.modmust end with/vN. - Every import of any package within the module must use the
/vN-suffixed path. - The Git tag is
vN.X.Y(without the suffix; the suffix is in the module path, not the tag name).
For modules at major version 0 or 1, the path must not include a /vN suffix.
The toolchain enforces these rules at build time:
- A
go.modwithmodule github.com/foo/bartaggedv2.0.0(without+incompatibleopt-out) is rejected: "module declares its path as github.com/foo/bar but was required as github.com/foo/bar/v2". - A
go.modwithmodule github.com/foo/bar/v2taggedv1.0.0is rejected: "v1.0.0 invalid: should be v2 or later, since the module declares the path with /v2 suffix".
+incompatible Rule¶
A module that has tags vN.X.Y for N >= 2 but does not declare a /vN suffix is referenced with +incompatible. Specifically:
- The toolchain searches Git tags for versions
vN.X.YwhereNmatches the requirement. - If found, but the module path declared in
go.moddoes not end with/vN, the version is annotated+incompatibleingo.modandgo.sum. +incompatibleversions interoperate as a single import path with thev0.x.yandv1.x.yof the same module (no multi-major coexistence).
The marker is informational. It states: "the maintainer did not adopt SIV for this major; expect breakage if you upgrade silently."
go.mod Directives Related to Versioning¶
| Directive | Syntax | Meaning |
|---|---|---|
module | module path | Declares the module path, including any /vN suffix for N >= 2. |
go | go MAJOR.MINOR | Declares the minimum Go language version. Affects lazy-loading behaviour at 1.17+. |
toolchain | toolchain go1.21.0 | (Optional, since Go 1.21.) Pins a specific toolchain. |
require | require path version | Pins a dependency. May be a real version or a pseudo-version. |
replace | replace path [version] => path [version] | Redirects an import path. Only the main module's replace is honoured. |
exclude | exclude path version | Forbids a specific version from MVS. Only the main module's exclude is honoured. |
retract | retract version or retract [v1, v2] | Marks a version (or range) of this module as not-recommended. |
Indirect dependencies are marked with // indirect after the require line. From go 1.17 on, every transitively-required module appears as an explicit require (often // indirect).
Minimum Version Selection Definition¶
The cmd/go documentation defines MVS as follows. Given:
M= the main module.Reqs(m)= the function returning the requirement list of module-versionm.Vers(p)= the set of available versions of module pathp.
Define Build(M) as the smallest set S of module-versions such that:
M ∈ S.- For every
m ∈ Sand every(p, v) ∈ Reqs(m): there exists somem' ∈ Swithm'.Path = pandm'.Version >= v. - For every path
psuch that some module inSrequires pathp: exactly one module-versionm ∈ Swithm.Path = p. - The version
m.Versionfor each pathpis the maximumvsuch that(p, v) ∈ Reqs(x)for somex ∈ S.
Equivalently, Build(M) is computed by the algorithm in professional.md: repeatedly take the highest required version for each path until the queue is empty.
The result is unique for any valid input — there is no choice point and no backtracking.
replace and exclude directives in the main module's go.mod modify the inputs to MVS:
replace p [v1] => q [v2]rewrites every requirement onp(or(p, v1)if a version is given) into a requirement on(q, v2).exclude p vremoves(p, v)from the input space; if aReqsentry would force it, MVS picks the next-higher available version.
Authoritative References¶
- The Go modules reference at
go.dev/ref/mod. Sections: "Version queries", "Pseudo-versions", "Module compatibility and semantic versioning", "Resolving a package to a module", "Minimal version selection". - The upstream
semver.org2.0.0 specification. Go applies it strictly with the additionalvprefix and SIV requirements. - The Go blog post "Go Modules: v2 and Beyond" — informal but clarifying.
- The package
golang.org/x/mod/semver— the canonical implementation of version parsing and comparison. - The package
golang.org/x/mod/module— module path validation and pseudo-version helpers.
For implementation details (MVS, proxy protocol, go.sum computation), see professional.md.