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 simplemainpackage 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.Printlnto print the proverb returned byproverb.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: