The moment a tag leaves your repository
You ship v1.0.0 of a library. Two hours later, a production service crashes because of a nil pointer dereference in your parsing logic. You need to push a fix. In package managers like npm or pip, you might publish a new version and hope downstream projects update. In Go, the toolchain is stricter. It expects semantic versioning, it enforces module path rules, and it treats version tags as immutable contracts. You cannot overwrite a tag. You cannot delete a published version from the module proxy. The only way to tell downstream projects to stop using a broken release is to publish a new one and mark the old one as retracted.
Semantic versioning in Go follows the standard MAJOR.MINOR.PATCH pattern, but the Go team added hard rules that trip up developers coming from other ecosystems. The version lives in a git tag, not in the source code. The go.mod file declares the module path and the minimum Go version required. When you tag v1.2.3, the go command reads that tag, builds the module, and uploads it to the proxy. The proxy then serves it to anyone who runs go get.
Go enforces a strict boundary at version one. Before v1.0.0, you are in the experimental phase. You can break APIs, change function signatures, and rewrite internals without penalty. Once you cross into v1.x.x, the public API becomes frozen. Any breaking change requires a major version bump and a change to the module path itself. This rule keeps dependency trees stable and prevents silent breakages across large codebases.
Treat version tags as immutable contracts. Once a tag leaves your repository, it belongs to the ecosystem.
How the toolchain reads your version
The go.mod file is the source of truth for your module. It tells the compiler where to find your code, what Go version you need, and which other modules you depend on. The file lives at the root of your repository. Every commit that gets tagged must include the correct go.mod. The proxy does not run your code. It only stores the source tarball and the module file. This design means the toolchain can verify your dependencies before they ever touch a developer's machine.
Here is the baseline go.mod for a fresh library.
module example.com/mylib
go 1.21
// The module path matches the git repository URL.
// It must be unique across the entire Go ecosystem.
// The go directive sets the minimum compiler version.
// Dependencies would be listed below this line.
// The toolchain refuses to build if the local version
// is older than the number declared here.
When a downstream project runs go get example.com/mylib@v1.0.0, the toolchain contacts the module proxy. The proxy checks its cache. If the version is missing, it clones your repository, checks out the v1.0.0 tag, reads go.mod, and archives the source. The proxy then returns the archive to the requester. The requester stores it in the local module cache and updates their own go.mod and go.sum files. The go.sum file contains cryptographic hashes of every downloaded module. It prevents supply chain attacks by ensuring the exact bytes you tested are the bytes you build.
The toolchain also enforces a naming convention for exported symbols. Public functions, types, and variables must start with a capital letter. Private names start lowercase. This rule applies to versioning too. When you publish v1.0.0, every capitalized name in your package becomes part of the public contract. Changing a capitalized name in a patch release breaks downstream builds. The compiler will reject the update with undefined: Package.PublicFunction or too many arguments in call to. You cannot sneak breaking changes into minor or patch versions.
Version numbers are promises. Keep them small, keep them honest.
Patching a release and retracting the broken one
You found the bug. You fixed it. Now you need to release v1.0.1 and tell the ecosystem to ignore v1.0.0. The retract directive handles this. It does not delete the old version. It simply tells go get to skip it during automatic upgrades. Downstream projects that explicitly pin v1.0.0 will keep using it until they manually update. Projects that run go get -u will jump straight to v1.0.1.
Here is the complete workflow for patching a released version and marking the broken release as retracted.
module example.com/mylib
go 1.21
// Retract tells the toolchain to ignore this version
// during automatic upgrades. Downstream projects will
// skip it when running go get -u. You can add a
// comment explaining the reason, though the toolchain
// ignores the comment and only reads the version.
retract v1.0.0
After updating go.mod, you commit the change and create a new tag. The tag must follow semantic versioning exactly. The go toolchain expects the v prefix. It expects three numeric components separated by dots. It rejects malformed tags with a clear error.
# Stage the updated module file and any source changes
git add go.mod
# Commit with a clear message referencing the fix
git commit -m "fix: correct nil pointer in ParseConfig"
# Tag the new patch version. The go toolchain requires
# the v prefix for all semantic versions. Missing it
# will cause the proxy to reject the release.
git tag v1.0.1
# Push the commit and the tag to the remote repository
git push origin main v1.0.1
Once the tag reaches the remote, the proxy will eventually index it. The proxy runs periodic crawlers. It usually picks up new tags within a few minutes. You can verify the retraction is visible by listing available versions.
# Fetch the version list from the proxy. The retracted
# version will appear in the output but will be marked
# as retracted in the go.mod of any project that pulls
# the latest metadata.
go list -m -versions example.com/mylib
The retract directive supports ranges too. You can write retract [v1.0.0, v1.0.2] to mark multiple versions as deprecated at once. This is useful when a security vulnerability spans several releases. The toolchain treats the range as a closed interval. It skips every version inside the brackets during dependency resolution.
Retraction is a signal, not a deletion. Downstream projects still need to update their go.mod files to move past the broken version.
Where things go wrong
Module versioning breaks when developers treat git tags like draft commits. The most common mistake is tagging a branch before committing go.mod. The proxy clones the repository, checks out the tag, and finds an outdated module file. The build fails. The toolchain rejects the version with go: errors parsing go.mod: missing go.mod file or module path mismatch. The version becomes unusable until you delete the tag, commit the correct file, and push a new tag. Deleting tags is allowed, but it leaves a gap in the proxy cache that takes time to clear.
Another frequent error is breaking the API in a patch release. The Go toolchain does not check for API compatibility. It trusts you. If you change a public function signature in v1.0.1, downstream builds will fail with cannot use type A as type B in argument or not enough arguments in call to. You must bump the major version instead. Major versions require a path suffix. v2.0.0 must live at example.com/mylib/v2. Every import statement in your code and in downstream projects must include the /v2 suffix. Forgetting this suffix breaks the build. The compiler will complain with module example.com/mylib provides example.com/mylib/v2 and is replaced but not required.
Pre-v1 versions follow different rules. You can publish v0.1.0, v0.2.0, v0.9.9 without changing the module path. The toolchain treats all v0.x.x releases as experimental. Downstream projects can upgrade automatically without fear of breaking changes. Once you hit v1.0.0, the rules lock in. You cannot go back to pre-v1 flexibility.
The proxy also caches aggressively. If you push a tag and immediately run go get, you might get a stale error. The proxy has not crawled your repository yet. Wait a few minutes or use GONOSUMCHECK only in controlled environments. Never disable checksum verification in production workflows. The go.sum file exists to protect you from tampered archives.
The proxy trusts your tags. If you break the rules, the ecosystem breaks with you.
Choosing the right version bump
Versioning is a coordination problem. You need to signal how much your code changed without forcing unnecessary updates. The Go community follows a strict convention. Pick the smallest bump that accurately describes the change.
Use a patch version when you fix a bug without changing the public API. Use a minor version when you add new functions or fields that do not break existing callers. Use a major version when you remove or rename public symbols, change function signatures, or alter package behavior. Use the retract directive when a published version contains a critical bug or security vulnerability that requires immediate downstream migration. Stick with pre-v1 versions when the library is still experimental and you expect frequent breaking changes.
Version numbers are promises. Keep them small, keep them honest.