I think I'll just summarize my findings down here; it didn't all seem obvious and took some googling and tinkering, as some sources are a bit ambiguous on some of the points.
The best practice as endorsed by the Go team (golang/go#25922 (comment)).
//go:build tools
// +build tools
package yourpackage
import (
_ "golang.org/x/tools/cmd/stringer"
// ...
)Change yourpackage to the name of the package that you put this in, e.g., use qux if you put it at the root of your module foo/bar/qux. Only use tools when you actually put it in a tools package (directory).
If you're on Go 1.17+, you don't need the // +build tools line.
To add the dependencies and the latest versions of these tools to your go.mod and freshen the module cache, run
go mod tidyIf you use gopls, configure it to include the tools build tag, e.g., for VSCode, add the following to your settings.json:
"gopls": {
"build.buildFlags": ["-tags=tools"],
}You can install the go.mod versions of all the tools to your $GOBIN directory (probably not the best idea, expand for details)
go install $(go list -f '{{join .Imports " "}}' tools.go)Note that this can break things for you well outside the module that you run this in, so you're likely going to be better off installing specific versions of tools manually, at your discretion.
If you still feel like you want to have the tools built (they really do start faster that way), either of the following sections (on Makefile and bingo) might be right up your alley.
Makefiles are very project-specific and everyone's setup is different, but I've found it useful to add this to my Makefile:
toolsGo := tools.go
toolsDir := bin/tools
toolPkgs := $(shell go list -f '{{join .Imports " "}}' ${toolsGo})
toolCmds := $(foreach tool,$(notdir ${toolPkgs}),${toolsDir}/${tool})
$(foreach cmd,${toolCmds},$(eval $(notdir ${cmd})Cmd := ${cmd}))
go.mod: ${toolsGo}
go mod tidy
touch go.mod
${toolCmds}: go.mod
go build -o $@ $(filter %/$(@F),${toolPkgs})
tools: ${toolCmds}
.PHONY: toolsIf you decided to put your tools.go in a tools package, you would have to change the toolsGo variable to tools/tools.go. It may also make sense to flip the toolsDir variable to tools/bin. And maybe put this snippet in a tools/tools.mk, that you could include to reduce the clutter in your main Makefile.
You'd probably want to add the toolsDir directory to your .gitignore.
The magic $(foreach ... $(eval ... line defines <toolname>Cmd variables (e.g. stringerCmd, mockgenCmd, etc.) to be used in other recipes in your Makefile. For (a very rudimentary) example:
wire: ${wireCmd}
${wireCmd} ./...
.PHONY: wireRunning make wire would now
- run
go mod tidyto ensurego.modis up to date withtools.go(if the latter is modified more recently than the former) - build
wireat the version that's defined in yourgo.mod(if it's not already built), and, finally, - recursively generate all your provider and injector wiring.
You can also execute
make toolsand, if go.mod is older than tools.go, it will run go mod tidy, after which all the tools that you have defined in tools.go will get built under the toolsDir (as bin/tools/stringer, for example).
You can use bingo to install version-suffixed executables of your module's tools in the global $GOBIN directory. This is an entirely different approach that avoids conflicts among tools and tool dependencies in larger projects, but it doesn't integrate with your module's go.mod (which might also be a good thing).
The author introduced this tool in golang/go#25922 (comment) and further expanded on it in golang/go#25922 (comment)