Managing your Go tool versions with
go.mod and a
When working with Go codebases, it's likely that you'll be delegating some functionality out to helper tools to make your life easier.
Something that's recommended on the Wiki and that I've seen across a few projects is the idea of a
As explained in more depth in Manage Go tools via Go modules, this gives you a central place to look for + manage dependencies.
However, this, and the example in the wiki fall down on is that it doesn't work super well 😅
If we take the example project, we receive an error if we don't pre-install the
$ go get
$ go generate
painkiller.go:5: running "stringer": exec: "stringer": executable file not found in $PATH
This isn't super helpful, as it requires we do some work up-front to get the commands prepared. This means new developers, as well as automated build environments, need to have done some work to get started, which may not even be consistent across repositories.
So what are our options for managing this better, and making it easier to get started?
One approach is to have a
Makefile task that allows you to parse the
tools.go and install those dependencies, but it's a little awkward, and I tend to try and avoid parsing complex text with things like
This approach isn't ideal, and leads to another command needing to execute before we get started, as well as depending on an arguably brittle text parsing approach.
Alternatively, because we've already got the dependencies and their versions pinned in our
go.mod, through the declaration in the
tools.go, we can actually get rid of the
To do this explicitly, we'd create a
tools.go with the following in it:
// +build tools
Thanks to this comment on GitHub, we can replace our invocations of the command-line application with a
go run invocation on the package, like so:
-//go:generate stringer -type=Pill
+//go:generate go run golang.org/x/tools/cmd/stringer -type=Pill
This is true whether they're in a
Makefile, a standalone script, or in our code.
This gives us the benefit of being purely managed through our
go.mod, meaning we can get tools like Dependabot to manage our dependency updates for us, too!
Note that there is a slight performance hit here, as
go run does not cache the built binary, at least as of Go 1.20. There is a proposal to track tool dependencies in
go.mod, which additionally discusses allowing caching for
go runs for the purpose of build tooling.
In my experience, the performance hit is negligible, but if you're not seeing the same, you can look at how to
go install via the