Not sure how to structure your Go web application?

My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

How to manage tool dependencies in Go 1.24+

Published on:
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.



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

File: go.mod
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.

File: main.go
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.