Managing your Go tool versions with go.mod and a tools.go

Featured image for sharing metadata for article

When working with Go codebases, it's likely that you'll be delegating some functionality out to helper tools to make your life easier.

For instance, you may be generating code with mockgen or oapi-codegen, or linting your project with golangci-lint.

Something that's recommended on the Wiki and that I've seen across a few projects is the idea of a tools.go.

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 stringer command.

$ 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?

Makefile

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 sed or awk.

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.

go.mod

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 Makefile magic.

To do this explicitly, we'd create a tools.go with the following in it:

//go:build tools
// +build tools

package main

import (
	_ "golang.org/x/tools/cmd/stringer"
)

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!

Performance

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

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #go.

Also on:

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.