How to use go run to manage tool dependencies
When you're working on a project it's common for there to be some developer tooling dependencies. These aren't code dependencies, but rather tools that you run as part of the development, testing, build or deployment processes.
For example, you might use
golang.org/x/text/cmd/gotext in conjunction with
go:generate to generate message catalogs for translation, or
honnef.co/go/tools/cmd/staticcheck to perform static analysis on your code before committing a change.
This raises a couple of interesting questions — especially in a team environment. How do you make sure that everyone has the necessary tools installed on their machines? And that the tools they are using are all the same version?
Until Go 1.17, the convention for managing this was to create a
tools.go file in your project containing
import statements for the different tools and a
//go:build tools build constraint. If you're not already familiar with this approach, it's described in the official Go Wiki.
But since Go 1.17 there is an alternative approach you can take. It has pros and cons compared to the
tools.go approach, but it's worth knowing about and may be a good fit for some projects.
It hinges on the fact that
go run now allows you to execute a specific version of a remote package. From the 1.17 release notes:
go run now accepts arguments with version suffixes (for example, go run email@example.com). This causes go run to build and run packages in module-aware mode, ignoring the go.mod file in the current directory or any parent directory, if there is one.
In other words, you can use
go run package@version to execute a remote package when you are outside of a module, or inside of a module even if the package isn't listed in the
It's also useful as a quick way to run an executable package without installing it. Instead of this:
You can now just do this:
Using with go:generate
Let's take a look at an example where we use the
golang.org/x/tools/cmd/stringer tool in conjunction with
go:generate to generate
String() methods for some
If you'd like to follow along, please run the following commands:
And then add the following code to
The important thing here is the
//go:generate line. When you run
go generate on this file, it will in turn use
go run to execute
v0.1.10 of the
Let's try it out:
You should see that the necessary modules are downloaded and then the
go:generate command finishes executing successfully — resulting in a new
level_string.go file being generated and a working application. Like so:
Using in a Makefile
You can also use the
go run package@version pattern to execute tools from your scripts or Makefiles. To illustrate, let's create a Makefile with an
audit task that executes a specific version of the
If you run
make audit, the necessary modules will be downloaded and the
staticcheck tool should complete its checks successfully.
If you run it for a second time, you'll see that the module cache is used and it should finish much faster.
Pros and cons
In terms of positives,
go run package@version has a couple of nice advantages over the
It's simpler to set up and requires less code — no
tools.gofile is needed, there are no build constraints, and no aliased imports.
It avoids polluting your dependency graph with things that your binaries do not actually depend on.
In terms of negatives:
If you have the same
go run package@versioncommand in multiple places throughout your codebase and want to upgrade to a newer version, then you need to update all of the commands manually (or use
sedor find-and-replace). With the
tools.goapproach you only need to update your
go.modfile by running
go get package@newversion.
tools.goapproach it's possible to verify that cached code in your module cache hasn't been changed by running
go mod verify. I'm not aware of an equivalent check for
go run package@version(if you know of a way to do this, please let me know!). From my limited testing, it seems to be possible to edit the cached code in the module cache on your machine, and
go run package@versionwill use this edited code without complaining.
If you are working offline, then
go run package@versionmay fail with a
dial tcp: lookup proxy.golang.org: Temporary failure in name resolutionerror because it can't reach the Go module mirror — even if there is a copy already in your local module cache. Similar to this:
As far as I can see this isn't a problem when you use the
tools.goapproach, although you can work around it fairly easily by setting the
GOPROXYenvironment variable to
directwhile you are offline. Doing this will force
go runto bypass the Go module mirror and use the cached module on your machine straight away.