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 simplemain
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 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: