4 Ways of Bumping Go Major Version
I've recently found myself in a rabbit hole of Go major version bumping to v2. What started as a simple task quickly turned into hours of sifting through conflicting information. Should I use a v2 directory? Create a new v2 branch? What about creating a new repository altogether?
The more I read, the more confused I became. To save future me from this headache, I've decided to jot down the differences that I've learned. I think this will be especially useful if you're maintaining a library that other developers depend on.
What I'll Cover
- 4 different approaches to major version bumping in Go
- Show real-world examples from popular Go libraries using each approach
- Tradeoffs of each approach
Approach 1: Major Version Subdirectory
Letβs start with the official recommendation from the Go team (reference). In this approach, we create a new subdirectory for each major version in your repository while the root directory maintains the previous version's code (v0 or v1).
How it works
- Make a new folder named after the new major version (e.g.
v2/
,v3/
, etc.) - Copy your code into this new folder
- Update the
go.mod
file in the new folder to include the new major version suffix in the module path - Git tagging the commit that represents the new major version release
Here's a simple visual:
github.com/user/project (main branch)
β
βββ go.mod # module github.com/user/project
βββ main.go
βββ utils.go
β
βββ v2/
β βββ go.mod # module github.com/user/project/v2
β βββ main.go
β βββ utils.go
β
βββ v3/
βββ go.mod # module github.com/user/project/v3
βββ main.go
βββ utils.go
In essence, we have a separate copy of the entire codebase for each major version in a separate subdirectory.
When to use
βThis approach is compatible with tools that arenβt aware of modules: file paths within the repository match the paths expected by go get in GOPATH mode. This strategy also allows all major versions to be developed together in different directories β¦ We recommend that module authors follow this strategy as long as they have users developing in GOPATH mode.β β Go Modules: v2 and Beyond
βIn other words, for every major version, we are encouraged to maintain a new copy of the entire codebase. This is also the only way to do it if you want pre-modules users to be able to use your package. I understand why for large projects this makes a ton of sense, it allows the maintainers to continue patching old versions easily while developing the new version.β β Goβs Major Versioning Sucks β From a Fanboy
Tradeoffs
Pros:
- Your code will work with older Go versions (pre-Go 1.11). This is especially useful in organizations that adopted Go before the pre-Go-module days who are still relying on GOPATH development mode (now legacy) as it relies on this specific directory structure
- Allows concurrent development of multiple major versions
Cons:
- Code duplication (also commonly shared code in the repository needs to be more carefully managed)
- More complex repository structure
Approach 2: Major Version Branch
The second commonly known strategy for bumping major versions in Go is the branch-based approach (reference). Instead of using directories, we maintain different major versions in separate Git branches.
How it works
- Create a new branch for the new major version (e.g.,
git checkout -b v2
) - In each branch, update the
go.mod
file to include the new major version suffix in the module path - Git tag releases in the new branch (e.g.,
v2.0.0
)
Here's a simple example:
github.com/user/project (v3 branch)
β
βββ go.mod # module github.com/user/project/v3
βββ main.go
βββ utils.go
github.com/user/project (v2 branch)
β
βββ go.mod # module github.com/user/project/v2
βββ main.go
βββ utils.go
github.com/user/project (v0 or v1 branch)
β
βββ go.mod # module github.com/user/project
βββ main.go
βββ utils.go
Tradeoffs
Pros:
- Clearer separation in version control
- Cleaner repository structure without duplicated directories
Cons
- May complicate CI/CD pipelines
- Potential confusion between branches and tags
Approach 3: Major Version Suffix
This approach offers a much simpler alternative to the previous approaches. Instead of creating new directories or branches, we simply increment the major version number suffix in the module path of your go.mod
file and thatβs it!
How It Works
- Keep your existing directory structure
- Update the
go.mod
fileβs module path to include the new major version suffix - Tagging the commit that represents the new major version release
Here's an example:
github.com/user/project (main branch)
β
βββ go.mod # module github.com/user/project/v2 (Changes with each major version)
βββ main.go
βββ utils.go
Tradeoffs
Pros:
- Simple and straightforward
- No code duplication
- No need to create new directories or branches
Cons:
- Potentially breaks compatibility with users who are still using GOPATH development mode
Approach 4: New Repository for Each Major Version
While not officially recommended by Go, some teams opt to create an entirely new repository for each major version. This way, they get a clear separation between versions, although it does make things a bit trickier to handle.
It might seem a little strange at first, but when you think about it, a major version (like v2) of a Go module really is like starting a whole new Go module.
How it works
- Create a new repository for each major version of your module
- Start fresh with the new version in the new repository
- Ensure that the
go.mod
file module path suffix correctly reflects the new major version
Some example:
github.com/user/project (main branch)
β
βββ go.mod # module github.com/user/project
βββ main.go
βββ utils.go
github.com/user/project-v2 (main branch)
β
βββ go.mod # module github.com/user/project-v2
βββ main.go
βββ utils.go
github.com/user/project-v3 (main branch)
β
βββ go.mod # module github.com/user/project-v3
βββ main.go
βββ utils.go
Tradeoffs
Pros:
- Complete isolation between versions
Cons:
- Increased overhead in managing multiple repositories
- Cannot reuse existing CI/CD pipelines
Summary
Consider the size of your project and whether you still have users working in GOPATH mode. Think about how your team likes to work and go with the approach that best fits your unique situation.
Closing Thoughts
Unfortunately, the complexity of major version bumping in Go has an unintended side effect. It seems to sometimes make developers hesitant to take the leap for necessary major version updates. As a result, thereβs a chance breaking changes may sneak in with minor updates, which goes against the spirit of semantic versioning.