Bumping a Go library to a new major version is not just a git tag v2.0.0. Go’s Semantic Import Versioning makes the module path itself part of the version contract — so v2+ means rewriting go.mod, every internal import, and (often) reorganizing branches. This is the short, command-driven version of the official guide, with the operational steps that are easy to forget.
The Rule in One Line
For any major version greater than 1, the module path must end with /vN:
module example.com/user/mymodule/v2
v0 and v1 keep the bare path. From v2 onwards, the /vN suffix is mandatory — without it, go get will refuse to resolve the new tag.
Two Strategies
There are two supported layouts, and both are valid:
- Branch strategy — a dedicated
v2branch lives next tomain. Cleanest history, easiest day-to-day maintenance. My default. - Subdirectory strategy — a
v2/folder inside the same branch holds the new code. Useful when you want one branch to contain every supported version (handy for monorepos and for projects where v1 and v2 are actively co-developed).
I prefer the branch strategy for active projects: each major version has its own home, backports stay obvious, and CI matrices don’t need to special-case a subdirectory.
Branch Strategy — Commands
Starting from a clean working tree on the v1 branch:
# 1. Create the v2 branch
git checkout -b v2
# 2. Rewrite the module path
go mod edit -module example.com/user/mymodule/v2
# 3. Rewrite every internal import
# macOS:
find . -type f -name '*.go' -exec \
sed -i '' 's,example.com/user/mymodule,example.com/user/mymodule/v2,g' {} +
# GNU/Linux:
# find . -type f -name '*.go' -exec \
# sed -i 's,example.com/user/mymodule,example.com/user/mymodule/v2,g' {} +
# 4. Confirm everything still builds and tests pass
go build ./... && go test ./...
# 5. Commit, tag, push
git commit -am "release: bump module path to v2"
git tag v2.0.0
git push origin v2 --tags
The tag must point at a commit where go.mod already declares the /v2 path — otherwise the proxy will reject it.
Subdirectory Strategy — Commands
If you’d rather keep everything in main:
# 1. Copy current sources into a v2/ subdirectory
mkdir v2
cp -r *.go go.mod go.sum v2/
# 2. Rewrite the module path inside v2/
cd v2
go mod edit -module example.com/user/mymodule/v2
# 3. Rewrite imports inside v2/ only
find . -type f -name '*.go' -exec \
sed -i '' 's,example.com/user/mymodule,example.com/user/mymodule/v2,g' {} +
# 4. Tag from the root
cd ..
git add v2
git commit -m "release: introduce v2 subdirectory"
git tag v2.0.0
git push origin main --tags
The tag is still v2.0.0 (not v2/v2.0.0). The proxy resolves the /v2 suffix to the subdirectory automatically.
What Not to Forget
This is where most v2 releases stumble:
Promote v2 as the default branch. On GitHub or GitLab, change the default branch from
maintov2once v2 is stable. Contributors land on the active version; v1 stays available as a maintenance branch.Update the README import block. Every
go getuser copies it. If it still saysexample.com/user/mymodule, they’ll silently keep installing v1.Mark v1 as deprecated. Add a
// Deprecated: use example.com/user/mymodule/v2comment on the package doc, plus a banner in the v1 README. The Go toolchain surfaces these ingo vetand editor tooling.Use
retractfor broken releases. Never delete a tag — the proxy andgo.sumdatabases remember it. Add aretractdirective ingo.modinstead:retract v2.0.0 // Published with broken import path.Bump the
godirective ingo.modif v2 raises the minimum Go version. A major bump is the only painless time to do it.Update CI matrices, examples, generated code, and docs.
sedonly catches.gofiles — example READMEs, snippets in docs, and codegen templates need a separate sweep.Keep a v1 maintenance branch. Cut a
release/v1(or just leave the oldmain) for backports. Patch releases likev1.4.3get tagged from there, not fromv2.Tag from the right commit. The tagged commit must contain the
/v2module path. If you tag too early (before thego.modedit lands), the proxy will reject the version and you’ll have to retract it and re-tag withv2.0.1.Commit
go.sum. Don’t.gitignoreit; downstream consumers depend on the checksums.
Verifying the Release
Once pushed, confirm the proxy has indexed it:
# Module proxy returns the version list
curl https://proxy.golang.org/example.com/user/mymodule/v2/@v/list
# Toolchain resolves the new path
go list -m example.com/user/mymodule/v2@latest
# A clean install works end-to-end
go install example.com/user/mymodule/v2/cmd/mytool@v2.0.0
If the proxy returns 404 for several minutes, that’s normal — first-time fetches trigger an indexing job. If it still fails after that, check that the tag is on a commit whose go.mod declares the /v2 path.
Wrap-Up
The mechanical part of a major bump is short: edit go.mod, rewrite imports, build, tag. The risky part is everything around it — the default branch, the README, the deprecation note on v1. Go’s tooling will not warn you if you skip those, but your users will.