Why must a Go module's path and its import paths end in /v2 once it releases v2.0.0?
answer
- one version per import path
- a breaking change needs a new name
- the major version lives in the path
- v0 and v1 are exempt
- both majors can coexist in one binary
basics
~20 sGo treats each major version as a separate module. From v2 on, the module line in go.mod and every import of its packages must end in /v2, so v1 and v2 can live in one build.
solid answer
~40 sGo identifies a package by its import path, and the build list holds exactly one version per module path. Semantic import versioning resolves that by putting the major version into the path itself: from v2 onward the `module` line in `go.mod` must end in `/v2`, and callers write `import "example.com/lib/v2/thing"`. v0 and v1 are exempt and keep the bare path. The practical consequence is that v2 is not an in-place upgrade — every consumer edits import lines, and until they do they keep compiling against v1, which is deliberate. It also means one binary may legitimately link both `example.com/lib` and `example.com/lib/v2`, which is what makes an incremental migration possible. The standard library follows the same convention: `math/rand/v2` sits alongside `math/rand`.
code
mod · 10 lines// go.mod of the library itself
module example.com/lib/v2
go 1.27
// go.mod of a consumer that is migrating gradually
require (
example.com/lib v1.9.0
example.com/lib/v2 v2.3.0
)go deeper
Be ready to state the rule cleanly: from v2 onward the module path and the import paths gain a /v2 element, while v0 and v1 use the bare path. Knowing that v2 is not an automatic upgrade is the part interviewers listen for.
Explain the mechanism behind the rule — one version per module path in the build list, so a breaking change must become a new path. Be able to say what breaks when a repo tags v2.0.0 without editing its module line.
Show you have migrated a real codebase across a major: both paths required at once, package-by-package rewrite, and the seams where a v1 value meets a v2 function. Mention that go get -u never carries a team across on its own.
Own the consequence for your organisation: a /v2 reaches nobody until every consumer edits imports, so the cost lands on the teams you depend on you. Be ready to say when you would keep a change inside v1 instead.
## The problem it solves In Go, a package is named by its **import path**, and the resolved build list contains at most one version of any given module path. If `example.com/lib` could mean either the v1 API or an incompatible v2 API, then two dependencies of your program that each need a different one could never be satisfied at the same time, and worse, upgrading a transitive dependency could silently swap the meaning of an import you never touched. Go's answer is the **import compatibility rule**: *if an old package and a new package have the same import path, the new package must be backwards compatible with the old one*. A breaking change therefore has to be a new import path. ## The rule From major version 2 onward, a module's path must end in a **major version suffix** matching the version it releases: - `example.com/lib` releases v0.x.y and v1.x.y - `example.com/lib/v2` releases v2.x.y - `example.com/lib/v3` releases v3.x.y The suffix appears in three places that must agree: the `module` line of the module's own `go.mod`, the `require` lines of everyone who depends on it, and every `import` statement that reaches into its packages. A package that lived at `example.com/lib/store` in v1 is imported as `example.com/lib/v2/store` in v2. ## The v0 and v1 exemption v0 and v1 use the bare path with no suffix. The reasoning differs for each: - **v1** is exempt for continuity — module paths predate modules, and the enormous body of pre-existing code that imports `example.com/lib` is, by the rule above, v1-compatible code. Making v1 write `/v1` would have broken every import in the ecosystem for no gain. (`example.com/lib/v1` is not an alias for the bare path; it is simply a different, usually non-existent, path.) - **v0** is exempt because SemVer gives v0 no compatibility promise at all. A v0 module may break on any minor release, so no path change is required — and that is exactly why staying at v0 is a signal to consumers that the API is still moving. ## What it feels like in practice Because `example.com/lib` and `example.com/lib/v2` are *different module paths*, the go command sees two unrelated modules. Consequences worth internalising: 1. **`go get -u` never crosses a major boundary.** Updating your dependencies moves you to the newest v1.x, forever. Adopting v2 is an explicit act: `go get example.com/lib/v2` plus an import rewrite. 2. **Both majors can be in one build.** `go.mod` may require `example.com/lib v1.9.0` and `example.com/lib/v2 v2.3.0` simultaneously, and the go command is perfectly happy — they are different modules. This is what lets a large program migrate package by package instead of in one commit. 3. **The compiler treats their types as unrelated.** A `lib.Config` and a `lib/v2.Config` are two distinct named types, because a type's identity includes the import path of the package that defines it. If a value has to cross a boundary between code on v1 and code on v2, you need an explicit conversion or an adapter. 4. **Tooling reads the suffix, not the tag alone.** If a repository tags v2.0.0 while its `go.mod` still says `module example.com/lib`, the go command rejects that version at the bare path and tells you the module path must end in `/v2`. The one exception is a repository with **no `go.mod` at all** at that tag, which is accepted at the bare path as a `+incompatible` version. ## Seen in the standard library The convention is not just for third parties. `math/rand/v2` was added in Go 1.22 as an incompatible redesign of `math/rand`, and it sits beside the original rather than replacing it, with both importable from the same program. That is semantic import versioning applied inside the standard library. ## The cost, stated honestly The rule buys reproducibility and the absence of diamond-version conflicts, and it charges for it in migration friction: a v2 release reaches nobody until each consumer edits their imports, and the library author now maintains two trees. That trade is the whole reason a Go library author thinks hard before cutting a v2 at all, and it is why so many Go libraries stay on v1 for years and add capability through new functions and option types rather than through a new major.
- Why are v0 and v1 exempt from the suffix?v1 is exempt for continuity: module paths predate modules, and all the code already importing `example.com/lib` is by definition v1-compatible, so requiring `/v1` would have broken the ecosystem for nothing. v0 is exempt because SemVer promises nothing at v0 — a v0 module may break on any release, so there is no compatibility claim for a path change to protect.
- Can one program import both example.com/lib and example.com/lib/v2?Yes, and it is a supported migration path. They are different module paths, so `go.mod` may require both and the linker includes both. The catch is that their types are unrelated and each has its own package-level state, so it works cleanly only while values do not cross between the two.
- Does go get -u move a consumer from v1 to v2?No. `go get -u` upgrades within a module path, and `example.com/lib/v2` is a different path, so a consumer on v1 stays on the newest v1.x indefinitely. Adopting v2 means requiring the `/v2` path explicitly and rewriting the import lines — the go command will never do it as part of a routine update.
saying these in an interview costs you the question
- Says the /v2 suffix is only a naming convention
- Claims go get -u upgrades a consumer to v2
- Thinks v1 modules must import as example.com/lib/v1
- Believes only one major of a library can be in a build
- Assumes tagging v2.0.0 is enough without editing go.mod