By Jay Conrod, software engineer on the Go tools team, and principal author of module retraction in the go command.

Module authors need a way to indicate that published module versions should not be used. There are a number of reasons this may be needed:

  • A severe security vulnerability has been identified.
  • A severe incompatibility or bug was discovered.
  • The version was published accidentally or prematurely.

Authors can’t simply delete version tags, since they remain available on module proxies. If an author were able to delete a version from all proxies, it would break downstream users that depend on that version.

Authors also can’t change versions after they’ve published. go.sum files and the checksum database verify that published versions never change.

Instead, authors should be able to retract module versions. A retracted module version is a version with an explicit declaration from the module author that it should not be used. (The word retract is borrowed from academic literature: a retracted research paper is still available, but it has problems and should not be the basis of future work).

Retracted versions should remain available in module proxies and origin repositories. Builds that depend on retracted versions should continue to work. However, users should be notified when they depend on a retracted version (either directly or indirectly). It should also be difficult to unintentionally upgrade to a retracted version.

This guide walks you through how to use module retractions. In the guide you will create two modules:

  • {{{.PROVERB}}} (part of a module at the same path) that provides various different wise proverbs. You will publish a number of versions of this module.
  • gopher, a simple main package that uses {{{.PROVERB}}}. You will not publish this module; it will be local-only.

Prerequisites

You should already have completed:

This guide is running using:

$ go version
go version go1.19.1 linux/amd64

The proverb module

Start by initialising your proverb module:

$ mkdir /home/gopher/proverb
$ cd /home/gopher/proverb
$ git init -q
$ git remote add origin https://{{{.PROVERB}}}.git
$ go mod init {{{.PROVERB}}}
go: creating new go.mod: module {{{.PROVERB}}}

Create an initial version with a Go function that returns a Go proverb in the file proverb.go:

package proverb

// Go returns a Go proverb
func Go() string {
	return "Don't communicate by sharing memory, share memory by communicating."
}

Commit and push this initial version:

$ git add -A
$ git commit -q -m "Initial commit"
$ git push -q origin main
remote: . Processing 1 references        
remote: Processed 1 references in total        

It’s early days for the proverb module, so you decide to tag it with a v0 semantic version, indicating it is not yet stable:

$ git tag v0.1.0
$ git push -q origin v0.1.0
remote: . Processing 1 references        
remote: Processed 1 references in total        

With the first version of the proverb module published, it’s time to create a first cut of your gopher module.

The gopher module

You are not going to publish the gopher module, so the setup is simpler:

$ mkdir /home/gopher/gopher
$ cd /home/gopher/gopher
$ go mod init gopher
go: creating new go.mod: module gopher

Notice how you skipped the git steps, and also how the module path is simply gopher. As this module will be local-only, you can use any path you like.

Create an initial version of the main function in gopher.go:

package main

import (
	"fmt"

	"{{{.PROVERB}}}"
)

func main() {
	fmt.Println(proverb.Go())
}

In this code you:

  • Import the {{{.PROVERB}}} package. This declares the dependency on this package.
  • Use the fmt.Println to print the proverb returned by proverb.Go() function.

Add a dependency on the {{{.PROVERB}}} module:

$ go get {{{.PROVERB}}}@v0.1.0
go: downloading {{{.PROVERB}}} v0.1.0
go: added {{{.PROVERB}}} v0.1.0

Run to make sure everything works:

$ go run .
Don't communicate by sharing memory, share memory by communicating.

Looks great!

A “better” initial version

After much debate with some friends, you decide that there is a better Go proverb to use in the initial version of your proverb module.

Change back to the proverb module:

$ cd /home/gopher/proverb

Update proverb.go as follows:

package proverb

// Go returns a Go proverb
func Go() string {
	return "Concurrency is parallelism."
}

Commit and push this updated version:

$ git add -A
$ git commit -q -m "Switch Go proverb to something more famous"
$ git push -q origin main
remote: . Processing 1 references        
remote: Processed 1 references in total        

Given there might be further changes to the proverb module before you mark it as v1 stable, you decide to publish v0.2.0 to be on the safe side:

$ git tag v0.2.0
$ git push -q origin v0.2.0
remote: . Processing 1 references        
remote: Processed 1 references in total        

With the second version of the proverb module now published, return to your gopher module and try this out:

$ cd /home/gopher/gopher
$ go get {{{.PROVERB}}}@v0.2.0
go: downloading {{{.PROVERB}}} v0.2.0
go: upgraded {{{.PROVERB}}} v0.1.0 => v0.2.0
$ go run .
Concurrency is parallelism.

Oops! That doesn’t look right: the proverb should read “Concurrency is not parallelism.” Looks like you made a mistake with the changes in v0.2.0. You can surely publish a new version of the proverb module to correct the mistake, but how can you prevent your users from depending on this broken version?

Module retraction to the rescue.

Retracting module versions

A module author can retract a version of a module by adding a retract directive to the go.mod file. The retract directive simply lists retracted versions. The go command reads retractions from the highest release version of the module.

To retract v0.2.0 of the proverb module, you will therefore publish v0.3.0. This new version will also fix the bug you found in v0.2.0.

Return to the proverb module:

$ cd /home/gopher/proverb

Rather than making the change to your go.mod file by hand, you can instead use go mod edit as follows:

$ go mod edit -retract=v0.2.0

Inspect the go.mod to see the result:

$ cat go.mod
module {{{.PROVERB}}}

go 1.19

retract v0.2.0

As is best practice, add a comment to the retract directive documenting why the retraction was necessary:

module {{{.PROVERB}}}

go 1.16

// Go proverb was totally wrong
retract v0.2.0

The comment may be shown by tools like go get and go list. It will be shown by gorelease and pkg.go.dev later on, too.

Fix the bug in proverb.go:

package proverb

// Go returns a Go proverb
func Go() string {
	return "Concurrency is not parallelism."
}

Commit, push and tag v0.3.0:

$ git add -A
$ git commit -q -m "Fix severe error in Go proverb"
$ git push -q origin main
remote: . Processing 1 references        
remote: Processed 1 references in total        
$ git tag v0.3.0
$ git push -q origin v0.3.0
remote: . Processing 1 references        
remote: Processed 1 references in total        

Return to your gopher module to use this new version:

$ cd /home/gopher/gopher
$ go get {{{.PROVERB}}}@v0.3.0
go: downloading {{{.PROVERB}}} v0.3.0
go: upgraded {{{.PROVERB}}} v0.2.0 => v0.3.0

This step also ensures that proxy.golang.org is now aware of v0.3.0. Manually “pulling” a version of a module into the proxy using go get in this way means you (or indeed others) do not have to rely or wait on the proxy automatically checking for later versions of a module.

Verify this new version fixes things:

$ go run .
Concurrency is not parallelism.

That’s better.

So what exactly has happened to v0.2.0? Remember, you can’t simply delete a version of the proverb module, since it remains available on module proxies. Let’s use go list to dig a bit deeper.

But first you need to wait for the proxy to update its state:

$ sleep 1m

Why? The FAQ on the proxy website explains:

In order to improve the proxy’s caching and serving latencies, new versions may not show up right away. If you want new code to be immediately available in the mirror, then first make sure there is a semantically versioned tag for this revision in the underlying source repository. Then explicitly request that version via go get module@version. After one minute for caches to expire, the go command will see that tagged version. If this doesn’t work for you, please file an issue.

List the non-retracted, “usable” versions of the {{{.PROVERB}}} module known to the proxy:

$ go list -m -versions {{{.PROVERB}}}
{{{.PROVERB}}} v0.1.0 v0.3.0

Notice that the newly published v0.3.0 is in this list, but v0.2.0 is not.

List all versions versions (including any that are retracted):

$ go list -m -versions -retracted {{{.PROVERB}}}
{{{.PROVERB}}} v0.1.0 v0.2.0 v0.3.0

Ah, there is the mischievous v0.2.0.

So what would happen if you were to rely on the now retracted v0.2.0?

$ go get {{{.PROVERB}}}@v0.2.0
go: warning: {{{.PROVERB}}}@v0.2.0: retracted by module author: Go proverb was totally wrong
go: to switch to the latest unretracted version, run:
	go get {{{.PROVERB}}}@latest
go: downgraded {{{.PROVERB}}} v0.3.0 => v0.2.0

A helpful message is printed, warning that you are now depending on a retracted version. Notice that this error message includes the text from the comment you added to the retract directive.

But does the code run?

$ go run .
Concurrency is parallelism.

Interesting: your code continues to “work” as before.

So how do you tell if you are relying on retracted versions of a module? For this, you again turn to go list:

$ go list -m -u all
gopher
{{{.PROVERB}}} v0.2.0 (retracted) [v0.3.0]

Let’s follow the advice of the warning message, and return to the latest un-retracted version:

$ go get {{{.PROVERB}}}@latest
go: upgraded {{{.PROVERB}}} v0.2.0 => v0.3.0

Another type of proverb

After consulting a wise friend, you decide that your proverb module needs to be a bit more rounded with its advice, and that one more change is therefore required before you declare it stable at v1. Returning to the proverb module:

$ cd /home/gopher/proverb

Update proverb.go as follows:

package proverb

// Go returns a Go proverb
func Go() string {
	return "Concurrency is not parallelism."
}

// Life returns a proverb useful for day-to-day living
func Life() string {
	return "A bird in the hand is worth two in the bush."
}

Commit and push this updated version:

$ git add -A
$ git commit -q -m "Add Life() proverb"
$ git push -q origin main
remote: . Processing 1 references        
remote: Processed 1 references in total        

To be safe, you decide to publish v0.4.0 before committing to a v1 stable version:

$ git tag v1.0.0
$ git push -q origin v1.0.0
remote: . Processing 1 references        
remote: Processed 1 references in total        

Oh no! You accidentally tagged and pushed v1.0.0 instead of v0.4.0. Users of your module might mistakenly think the proverb API is stable and that there will not therefore be any breaking changes. But you’re not ready to make that sort of commitment!

Let’s retract v1.0.0 of the proverb module, but first publish with the correct version, v0.4.0:

$ git tag v0.4.0
$ git push -q origin v0.4.0
remote: . Processing 1 references        
remote: Processed 1 references in total        

To retract v1.0.0 you will need to publish v1.0.1. But that means you will also need to retract version v1.0.1. You do so using a closed range. Make this change by hand by editing the proverb go.mod file, commenting the retract directive as is best practice:

module {{{.PROVERB}}}

go 1.16

retract (
	// Go proverb was totally wrong
	v0.2.0

	// Published v1 too early
	[v1.0.0, v1.0.1]
)

Commit, tag, push and publish v1.0.1:

$ git add -A
$ git commit -q -m "Retract [v1.0.0, v1.0.1]"
$ git push -q origin main
remote: . Processing 1 references        
remote: Processed 1 references in total        
$ git tag v1.0.1
$ git push -q origin v1.0.1
remote: . Processing 1 references        
remote: Processed 1 references in total        

Return to the gopher module to experiment with the newly retracted versions:

$ cd /home/gopher/gopher

Ensure proxy.golang.org is aware of the new versions of proverb you just published. go get v0.4.0 last to leave that version as the current dependency (you will test it later).

$ go get {{{.PROVERB}}}@v1.0.0
go: downloading {{{.PROVERB}}} v1.0.0
go: warning: {{{.PROVERB}}}@v1.0.0: retracted by module author: Published v1 too early
go: to switch to the latest unretracted version, run:
	go get {{{.PROVERB}}}@latest
go: upgraded {{{.PROVERB}}} v0.3.0 => v1.0.0
$ go get {{{.PROVERB}}}@v1.0.1
go: downloading {{{.PROVERB}}} v1.0.1
go: warning: {{{.PROVERB}}}@v1.0.1: retracted by module author: Published v1 too early
go: to switch to the latest unretracted version, run:
	go get {{{.PROVERB}}}@latest
go: upgraded {{{.PROVERB}}} v1.0.0 => v1.0.1
$ go get {{{.PROVERB}}}@v0.4.0
go: downloading {{{.PROVERB}}} v0.4.0
go: downgraded {{{.PROVERB}}} v1.0.1 => v0.4.0

Note: you might not see the warning about either v1.0.0 or v1.0.1 being retracted versions. proxy.golang.org automatically updates periodically; depending on how lucky you are, an automatic update may have occurred in the time since you published v1.0.1 in which case you will see the warning message, or it may not.

Now that you have pulled these versions through the proxy, wait for the proxy cache to update:

$ sleep 1m

List all versions of the {{{.PROVERB}}} known to the proxy, including the retracted ones:

$ go list -m -versions -retracted {{{.PROVERB}}}
{{{.PROVERB}}} v0.1.0 v0.2.0 v0.3.0 v0.4.0 v1.0.0 v1.0.1

List the non-retracted versions of the {{{.PROVERB}}} known to the proxy:

$ go list -m -versions {{{.PROVERB}}}
{{{.PROVERB}}} v0.1.0 v0.3.0 v0.4.0

This shows that v0.4.0 is the latest version of the proverb module, as expected.

Update gopher.go to use the new proverb:

package main

import (
	"fmt"

	"{{{.PROVERB}}}"
)

func main() {
	fmt.Printf("Go proverb: %v\n", proverb.Go())
	fmt.Printf("Life advice: %v\n", proverb.Life())
}

Finally, run gopher to verify everything works:

$ go run .
Go proverb: Concurrency is not parallelism.
Life advice: A bird in the hand is worth two in the bush.

Note that when you come to publish the “real” v1.0.0 of the proverb module, it must be published as v1.0.2 since you cannot change or delete versions (retracted or not).

Conclusion

That’s it! This guide has introduced module retraction, demonstrating how to retract a simple bad version, as well as retracting an accidental v1. For more information on module retraction, see the modules reference.

As a next step you might like to consider: