Effective versioning is crucial for managing dependencies, ensuring stability, and communicating changes in your Go projects. With the introduction of Go Modules, Go has a canonical way to handle versions. This guide outlines the best practices to follow.
Go Modules are the official dependency management system for Go and are central to versioning. If you're starting a new project or maintaining an existing one, ensure it's using Go Modules.
go.modfile: This file, located at the root of your module, defines the module's path (its unique identifier), the Go version it's built with, and its dependencies with their specific versions.- Initialization: Start using modules in your project with
go mod init <module_path>, for example,go mod init github.com/yourusername/yourproject.
Go modules use Semantic Versioning (SemVer) 2.0.0. Version numbers are typically in the format vMAJOR.MINOR.PATCH.
MAJORversion (e.g.,v1.0.0->v2.0.0): Incremented when you make incompatible API changes.MINORversion (e.g.,v1.0.0->v1.1.0): Incremented when you add functionality in a backward-compatible manner.PATCHversion (e.g.,v1.0.0->v1.0.1): Incremented when you make backward-compatible bug fixes.
Key SemVer Points in Go:
v0.y.z(Initial Development): Major version zero is for initial development. Anything may change at any time. The public API should not be considered stable.v1.0.0(First Stable Release): This version defines the first stable public API. From this point on, changes are governed by SemVer rules regarding backward compatibility.- Pre-release Versions: You can denote pre-releases by appending a hyphen and a series of dot-separated identifiers (e.g.,
v1.0.0-alpha,v2.1.0-beta.1). Pre-releases have lower precedence than their normal version (e.g.,v1.0.0-alphacomes beforev1.0.0). - Build Metadata: You can append build metadata with a plus sign (e.g.,
v1.0.0+build.123). Build metadata is ignored when determining version precedence.
When you release a new version of your module, you should create a Git tag with the semantic version number.
- Tagging:
git tag v1.0.1 git push origin v1.0.1
- Immutable Versions: Once a version is tagged and published, it should be considered immutable. Any changes must be released as a new version.
This is a critical aspect of Go versioning due to the "import compatibility rule."
When you release a v2 or higher major version (which implies breaking changes):
- Module Path Must Change: The module path in your
go.modfile must be updated to include the major version suffix. For example, if your module wasgithub.qkg1.top/yourusername/yourprojectforv0andv1versions, forv2.0.0and subsequentv2.x.xversions, the module path becomesgithub.qkg1.top/yourusername/yourproject/v2.// go.mod for v1.x.x module [github.com/yourusername/yourproject](https://github.com/yourusername/yourproject) go 1.21 // go.mod for v2.x.x module [github.com/yourusername/yourproject/v2](https://github.com/yourusername/yourproject/v2) go 1.21 - Import Path Updates: Consumers of your module will need to update their import paths in their Go source files to use the new major version.
// Importing v1 import "[github.com/yourusername/yourproject/somepackage](https://github.com/yourusername/yourproject/somepackage)" // Importing v2 import "[github.com/yourusername/yourproject/v2/somepackage](https://github.com/yourusername/yourproject/v2/somepackage)"
- A New Module: Effectively, a new major version (
v2,v3, etc.) is treated as a new, separate module by the Go toolchain.
Strategies for Structuring Major Versions in Your Repository:
-
Major Version Subdirectory (Recommended by Go Team):
- Create a new directory in your repository for the new major version (e.g.,
v2/,v3/). - Copy the
v1codebase into this new directory as a starting point. - The
go.modfile within this subdirectory will declare the/v2module path (e.g.,module github.com/yourusername/yourproject/v2). - The
v0/v1code can remain in the root directory or its ownv1/directory. - Tags for
v2releases would be likev2.0.0,v2.0.1, etc. (If thev2code is in av2/subdirectory, the Go toolchain understands this. Some projects tag likev2/v2.0.0but this is less common now). - Pros: Compatible with older Go versions (pre-Go 1.11 GOPATH mode), allows concurrent development of multiple major versions.
- Create a new directory in your repository for the new major version (e.g.,
-
Major Version Branch:
- Create a new Git branch for the new major version (e.g.,
v2-branch). - In this branch, update the
go.modfile to include the major version suffix in the module path. - Tag releases from this branch (e.g.,
v2.0.0). - Pros: Clearer separation in version control, cleaner repository structure without duplicated directories in the main branch.
- Cons: Users might need to be more aware of which branch to track for a specific major version if they are not just relying on
go get.
- Create a new Git branch for the new major version (e.g.,
Considerations for Major Version Updates:
- Disruption: Major version updates can be disruptive for your users. Only make them when absolutely necessary.
- Support: Clearly communicate your support policy for previous major versions.
- Maintenance: Be prepared to maintain multiple major versions if necessary (e.g., backporting security fixes).
When you go get a specific commit hash or a branch name that hasn't been tagged with a semantic version, Go tools will generate a "pseudo-version" in your go.mod file.
- Format:
vX.Y.Z-yyyymmddhhmmss-commitabbrev(e.g.,v0.0.0-20231027143000-abc123def456).vX.Y.Z-0: Base version from a preceding tag, orvX.0.0if no tag.yyyymmddhhmmss: UTC timestamp of the commit.commitabbrev: 12-character prefix of the commit hash.
- Usage: Useful for development or when you need a specific fix that isn't yet in a tagged release. However, for stable dependencies, always prefer tagged semantic versions.
- Avoid Manual Creation: Let Go tools generate pseudo-versions.
Go modules are typically published by pushing tags to a version control repository (e.g., GitHub, GitLab).
- Commit your changes.
- Tag the commit:
git tag vX.Y.Z - Push the tag:
git push origin vX.Y.Z
The Go proxy (and other users) will then be able to discover and download this version.
While the common practice is one module per repository, Go does support multiple modules within a single repository.
- Tag Prefixes: If you have modules in subdirectories (e.g.,
repo/modA/go.modandrepo/modB/go.mod), you need to prefix your version tags with the directory name:- For
modA:git tag modA/v1.0.0 - For
modB:git tag modB/v1.2.0
- For
- Complexity: This can add complexity to your build and release process. Consider carefully if this structure is necessary.
While not specific to Go versioning, good Git hygiene supports a clean versioning strategy:
- Commit Small, Logical Changes: Makes history easier to understand and revert if needed.
- Write Meaningful Commit Messages: Follow conventions like Conventional Commits to clearly explain changes. This can also help automate changelog generation.
- Use Branches: For features (
feature/my-new-feature), bug fixes (fix/that-annoying-bug), etc. - Pull Before You Push: Keep your local repository up-to-date to avoid merge conflicts.
- Use Pull Requests (PRs) / Merge Requests (MRs): Facilitate code review and discussion before merging changes.
By following these best practices, particularly adhering to Semantic Versioning and understanding how Go Modules handle major versions, you can create a robust and predictable versioning scheme for your Go projects. This benefits both you as the maintainer and the users of your module.