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 v2 branch lives next to main. 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 main to v2 once v2 is stable. Contributors land on the active version; v1 stays available as a maintenance branch.

  • Update the README import block. Every go get user copies it. If it still says example.com/user/mymodule, they’ll silently keep installing v1.

  • Mark v1 as deprecated. Add a // Deprecated: use example.com/user/mymodule/v2 comment on the package doc, plus a banner in the v1 README. The Go toolchain surfaces these in go vet and editor tooling.

  • Use retract for broken releases. Never delete a tag — the proxy and go.sum databases remember it. Add a retract directive in go.mod instead:

    retract v2.0.0 // Published with broken import path.
    
  • Bump the go directive in go.mod if 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. sed only catches .go files — 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 old main) for backports. Patch releases like v1.4.3 get tagged from there, not from v2.

  • Tag from the right commit. The tagged commit must contain the /v2 module path. If you tag too early (before the go.mod edit lands), the proxy will reject the version and you’ll have to retract it and re-tag with v2.0.1.

  • Commit go.sum. Don’t .gitignore it; 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.