Tools for Go modules

Conrad Irwin — December 2024

For the last ~two years I’ve been working on pushing for a small quality of life improvement for Go teams. I’m thrilled that modules can now declare tool dependencies starting with Go 1.24:

# go.mod
mod example.com/mod
go 1.24

tool (
  golang.org/x/tools/cmd/stringer
  ./cmd/migrate
)

require (...)

A common problem that development teams run into is the need to have a consistent development environment so that little time is spent debugging “works on my machine” kinds of issues.

For large centrally managed teams one solution is pre-imaged machines that have executables installed correctly, but for smaller setups without dedicated infrastructure teams or open source repos with minimal centralization this doesn’t make sense.

Many programming environments, like the node ecosystem for JavaScript, or the cargo ecosystem for Rust have a solution: in addition to being able to depend on libraries that will be linked into your program, you can depend on executable ‘tools’ that are used as part of the software development workflow.

This functionally is used for everything: linters, code generation, database management, deployments, etc.. For small fast moving teams this provides a lightweight way to ensure that everyone working on a given project has access to the right set of tools, and that as the project evolves, the tools evolve in parallel.

Now, starting with Go1.24 you can achieve the same thing!

When you add a tool directive to your go.mod, for example tool golang org/x/tools/cmd/stringer everyone who collaborates on a module can run go tool stringer to run the correct version of stringer. To add a new tool declaration and the necessary dependencies, use go get -tool golang.org/x/tools/cmd/stringer.

This works for tools built by other people, and also when the tool is defined in the module itself. For example if you have a module example.com/mod which contains cmd/migrate/ You can add a tool example.com/mod/cmd/migrate directive and then run go tool migrate . The advantage of doing this is that these tools will show up in the list of available tools with go tool.

Although it’s a comparatively minor change, it had some knock-on effects on the rest of the ecosystem that were worth discussing. Most controversial was that tools dependencies are resolved in your module graph and not their own. We talked about making this optional, or pushing to a world where tools used their own module graph for resolution.

  • Supporting both leads to a confusing user experience, so that was ruled out as the least good option.
  • Tool authors want to be able to provide assurance about the builds they provide; it’s frustrating to get bug reports from users only to find that they’ve built a version that differs in subtle ways from the original.
  • Tool users want a way to quickly verify whether security advisories affect them, and to upgrade packages without waiting for other parts of the ecosystem to respond.

We ended up prioritizing tool users’ security concerns over tool authors’ compatibility concerns. There are some secondary benefits (tools that generate code with imports know that the versions match), but this is also consistent with the broader go ethos: packages should not break backwards compatibility, and when they accidentally do it is a problem that requires the community to work together to fix.

Please give the new feature a whirl as you’re testing out the Go1.24 release candidates, and report bugs to the Go issue tracker.

I hope this is able to remove a small amount of friction for you and your collaborators!