How to manage tool dependencies in Go 1.24+
Level: Beginner
One of my favourite features of Go 1.24 is the new functionality for managing developer tooling dependencies.
By this, I mean tooling that you use to assist with development, testing, build, or deployment – such as staticcheck
for static code analysis, govulncheck
for vulnerability scanning, or air
for live-reloading applications.
Historically, managing these dependencies — especially in a team setting — has been tricky. The previous solutions have been to use a tools.go
file or the go run
pattern, but while these approaches work, they’ve always felt like workarounds with some downsides.
With Go 1.24, there’s finally a better way.
- Adding tools to your module
- Using tools
- Listing tools
- Verifying tools
- Vendoring tools
- Upgrading and downgrading tools
- Removing tools
A quick example
To demonstrate the new functionality, let's scaffold a simple module and add some application code.
$ go mod init example.com
go: creating new go.mod: module example.com
$ touch main.go
package main
import (
"fmt"
"github.com/kr/text"
)
func main() {
wrapped := text.Wrap("This is an informational message that should be wrapped.", 30)
fmt.Println(wrapped)
}
Now fetch the github.com/kr/text
package and run the code. The output should look like this:
$ go get github.com/kr/text
go: downloading github.com/kr/text v0.2.0
go: added github.com/kr/text v0.2.0
$ go run .
This is an informational
message that should be
wrapped.
Adding tools to your module
Go 1.24 introduces the -tool
flag for go get
, which you can use like this:
go get -tool import_path@version
This command will download the package specified by the import path (along with any child dependencies), store them in your module cache, and record them in your go.mod
file. The @version
part is optional – if you omit it, the latest version will be downloaded.
Let's use this to add the latest versions of stringer
and govulncheck
to our module as developer tools, along with staticcheck
version 5.0.1
.
$ go get -tool golang.org/x/tools/cmd/stringer
go: downloading golang.org/x/tools v0.30.0
go: downloading golang.org/x/sync v0.11.0
go: downloading golang.org/x/mod v0.23.0
go: added golang.org/x/mod v0.23.0
go: added golang.org/x/sync v0.11.0
go: added golang.org/x/tools v0.30.0
$ go get -tool golang.org/x/vuln/cmd/govulncheck
go: downloading golang.org/x/vuln v1.1.4
go: downloading golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7
go: downloading golang.org/x/sys v0.30.0
go: upgraded golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 => v0.0.0-20240522233618-39ace7a40ae7
go: added golang.org/x/vuln v1.1.4
$ go get -tool honnef.co/go/tools/cmd/staticcheck@v0.5.1
go: downloading honnef.co/go/tools v0.5.1
go: downloading golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: downloading github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: downloading golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
go: added github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: added golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: added honnef.co/go/tools v0.5.1
After running these, your go.mod
file will now include a tool (...)
section listing the tools you've added. The corresponding module paths and versions for all the dependencies will appear in the require (...)
section and be marked as indirect:
module example.com
go 1.24.0
require (
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/kr/text v0.2.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
golang.org/x/tools v0.30.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
honnef.co/go/tools v0.5.1 // indirect
)
tool (
golang.org/x/tools/cmd/stringer
golang.org/x/vuln/cmd/govulncheck
honnef.co/go/tools/cmd/staticcheck
)
Using tools
Once added, you can run tools using the go tool
command.
From the command line
To run a specific tool from the command line within your module, you can use go tool
followed by the last non-major-version segment of the import path for the tool (which is, normally, just the name for the tool). For example:
$ go tool staticcheck -version
staticcheck 2024.1.1 (0.5.1)
$ go tool govulncheck
No vulnerabilities found.
In a makefile
The go tool
command also works nicely if you want to execute tools from your scripts or Makefiles. To illustrate, let's create a Makefile with an audit
task that runs staticcheck and govulncheck on the codebase.
$ touch Makefile
.PHONY: audit
audit:
go vet ./...
go tool staticcheck ./...
go tool govulncheck
If you run make audit
, you should see that all the checks complete successfully.
$ make audit
go vet ./...
go tool staticcheck ./...
go tool govulncheck
No vulnerabilities found.
With go:generate
Let's also take a look at an example where we use the stringer tool in conjunction with go:generate
to generate String()
methods for some iota
constants.
package main
import (
"fmt"
"github.com/kr/text"
)
//go:generate go tool stringer -type=Level
type Level int
const (
Info Level = iota
Error
Fatal
)
func main() {
wrapped := text.Wrap("This is an informational message that should be wrapped.", 30)
fmt.Printf("%s: %s\n", Info, wrapped)
}
The important thing here is the //go:generate
line. When you run go generate
on this file, it will in turn use go tool
to execute the version of the stringer tool listed in your go.mod
file.
Let's try it out:
$ go generate .
$ ls
go.mod go.sum level_string.go main.go Makefile
You should see that a new level_string.go
file is created, and running the application should result in some output that looks like this:
$ go run .
Info: This is an informational
message that should be
wrapped.
Listing tools
You can check which tools have been added to a module by running go list tool
, like so:
$ go list tool
honnef.co/go/tools/cmd/staticcheck
golang.org/x/tools/cmd/stringer
golang.org/x/vuln/cmd/govulncheck
Verifying tools
Because the tools are included in your go.mod
file as dependencies, if you want to check that the code for the tools stored in your module cache has not changed you can simply run go mod verify
:
$ go mod verify
all modules verified
This will check that the code in your module cache exactly matches the corresponding checksums in your go.sum
file.
Vendoring tools
If you run go mod vendor
, the code for tooling dependencies will be included in the vendor
folder and the vendor/modules.txt
manifest alongside your non-tool dependencies.
$ go mod vendor
$ tree -L 3
.
├── go.mod
├── go.sum
├── main.go
├── Makefile
└── vendor
├── github.com
│ ├── BurntSushi
│ └── kr
├── golang.org
│ └── x
├── honnef.co
│ └── go
└── modules.txt
When tools are vendored in this way, running go tool
will execute the corresponding code in the vendor
directory. Note that go mod verify
does not work on vendored code.
Upgrading and downgrading tools
To upgrade or downgrade a specific tool to a specific version, you can use the same go get -tool import_path@version
command that you did for adding the tool originally. For example:
$ go get -tool honnef.co/go/tools/cmd/staticcheck@v0.5.0
To upgrade to the latest version of a specific tool, omit the @version
suffix.
$ go get -tool honnef.co/go/tools/cmd/staticcheck
You can also upgrade all tools to their latest version by running go get tool
. Note: tool
is a sub-command here, not a flag.
$ go get tool
If your tool dependencies are vendored, you will need to re-run go mod vendor
after any upgrades or downgrades.
At the time of writing, I'm not aware of any easy way to specifically list the tools that have upgrades available – if you know of one please let me know!
Removing tools
To remove the tool completely from your module, use go get -tool
with the special version tag @none
.
$ go get -tool honnef.co/go/tools/cmd/staticcheck@none
Again, if you're vendoring, make sure to run go mod vendor
after removing a tool.