I thought I would share some quick bits about how to do go.mod version bumps.

Genesis

Modules start in v0.0.0 by default. If you never tag them, their version will be something like v0.0.0-{timestamp}-{commithash}, for example:

require github.com/fake/mod v0.0.0-20240514230400-03fa26f5508f

If you want to tag a v0.1.0, for example, tagging is all that you need to do. Users would then be able to get it with:

go get github.com/fake/mod@v0.1.0

v1

You can tag v1.0.0 without any other changes:

Users would get it with:

go get github.com/fake/mod@v1.0.0

v2 and beyond

Now, here things start to get a bit more complicated.

Let’s say we want to tag v2.0.0. We will have to also change the module path, adding a /v2 to it.

module github.com/fake/mod/v2

Users would have to require the module adding the /v2 path, and the @v2.0.0 version:

module github.com/fake/app

require github.com/fake/mod/v2 v2.0.0

Users would get it with:

go get github.com/fake/mod/v2@v2.0.0

That looks a bit repetitive, I know.

If you want to keep both v1 and v2, you might want to create a v2 branch, and work on your v2 there. You can read more about it here.

Updating imports

One side effect of changing your module path is that you now need to change all your imports as well.

You need to be careful to do this before actually tagging, otherwise it might happen that your v2 will depend on your v1.

I recently launched GoReleaser v2, and had probably thousands of import statements to update.

I ended up updating them all with this script:

#!/usr/bin/env bash
#
# ./gomod-rename example.com/old/module example.com/new-module
#
go mod edit -module "${2}"
find . -type f -name '*.go' -exec sed -i -e "s,\"${1}/,\"${2}/,g" {} \;
find . -type f -name '*.go' -exec sed -i -e "s,\"${1}\",\"${2}\",g" {} \;

Run it as:

./gomod-rename github.com/goreleaser/goreleaser github.com/goreleaser/goreleaser/v2

And that was it.

Monorepos

At Charm, we have our very own charmbracelet/x repository, which contains many modules that we deem either not worth or not ready to be their own repositories yet.

Here’s an example go.mod:

module github.com/charmbracelet/x/ansi

To version it, it is recommended to include the module name in the tag:

git tag ansi/v0.1.0

Then, users would get it with:

go get github.com/charmbracelet/x/ansi@v0.1.0

I love that I don’t have to repeat the ansi/ bit in the version.

The Go module system knows to search for the ansi/ tag prefix. This is pretty much how GoReleaser’s monorepo feature works, by the way.

Finally, in the go.mod file, it’ll look like this:

require github.com/charmbracelet/x/ansi v0.1.0

Retracting versions

Sometimes you fuck things up. Just recently, I did fuck up in caarlos0/env: I pushed what could in some cases be a breaking change in a patch release, without realizing it.

Luckily, you can easily retract versions:

module github.com/caarlos0/env/v11

retract v11.0.1 // explain why it is being retracted.

Then, if users try to get it:

go: warning: github.com/caarlos0/env/v11@v11.0.1: retracted by module author: explain why it is being retracted.
go: to switch to the latest unretracted version, run:
go get github.com/caarlos0/env/v11@latest

This is a good way to communicate to your users when you accidentally pushed a bad version.


Fin

That’s all folks. 🎉

Just some quick tips, hopefully useful for more people than just myself a couple of months from now.