When it comes to Go, most of my experience has been writing CLI tools and a data processor that subscribed to new Ethereum blocks, fetched related data with goroutines, and exported the results to S3. None of these projects needed external build tools.
Lately, I’ve been writing backend CRUD apps in Go, which means using a database migration tool. That’s when I discovered something odd: the way tool versions are pegged in Go projects. There seem to be a couple of ways to do this.
The Recommended Way
From golang/go#25922, the best
practice endorsed by the Go team is
golang/go#25922 (comment),
that is creating /tools/tools.go
with the tools
tag:
//go:build tools
package tools
import (
_ "github.com/golang-migrate/migrate/v4/cmd/migrate"
)
There are a few things to note here:
-
The tag is used so that
go build
won’t compile into the binary. This feels strange to me 😁 I’m used to using Rustcfg
attributes for targeting different OS or building tests separately with#[cfg(test)]
- NOTE: Use
// +build tools
instead of//go:build tools
for Go 1.16 or lower
- NOTE: Use
-
Additionally,
gopls
will warn with:No packages found for open file /..../tools/tools.go.
which can be ignored in VSCode with:
"gopls": { "buildFlags": ["-tags=tools"] },
-
gopls
will also error with:import "github.com/golang-migrate/migrate/v4/cmd/migrate" is a program, not an importable package
because it’s a
main
package. It’s not possible to get rid of this error unless a newtools/go.mod
is added, meaningtools
become a standalone package. But this defeats the purpose of pegging tool versions as they can diverge from the root project.- NOTE: The error shows up only if you open the file directly in your editor. It doesn’t affect builds since the file is excluded by the build tag.
Makefile
Normally I run go
commands directly, but here a Makefile can help pin
versions:
MIGRATE_VER := v4.18.3
MIGRATE := github.com/golang-migrate/migrate/v4/cmd/migrate@$(MIGRATE_VER)
install-tools:
@go install $(MIGRATE)
- NOTE:
go install module@version
works for Go 1.17+ - The Makefile installs globally but it can be installed into a custom
$GOBIN
This will probably become a problem if the project needs to import migrate
for
running migration as part of startup, for example. In that case, the version
would need to be parsed from go.mod
.
Bingo
Another popular option is bingo, which
manages tool dependencies in a separate mod
file. It’s more structured but
adds another layer of tooling.
TL;DR
In Go you can pin tools in three ways:
tools.go
with a build tag (Go team’s recommended way)- Hardcode versions in a Makefile
- Use
bingo
I’ll stick with the recommended way.