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 example.com/cmd@v1.0.0). 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 go.mod
file.
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 iota
constants.
If you'd like to follow along, please run the following commands:
And then add the following code to main.go
:
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 golang.org/x/tools/cmd/stringer
package.
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 staticcheck
tool.
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 tools.go
approach:
It's simpler to set up and requires less code — no
tools.go
file 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@version
command 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 usesed
or find-and-replace). With thetools.go
approach you only need to update yourgo.mod
file by runninggo get package@newversion
.With the
tools.go
approach it's possible to verify that cached code in your module cache hasn't been changed by runninggo mod verify
. I'm not aware of an equivalent check forgo 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, andgo run package@version
will use this edited code without complaining.If you are working offline, then
go run package@version
may fail with adial tcp: lookup proxy.golang.org: Temporary failure in name resolution
error 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.go
approach, although you can work around it fairly easily by setting theGOPROXY
environment variable todirect
while you are offline. Doing this will forcego run
to bypass the Go module mirror and use the cached module on your machine straight away.