skip to content

Search

Go please ignore tools

2 min read

How to pin tool versions in Go projects: tools.go, Makefile installs, and bingo

# backend
# go
Not in series

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.

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 Rust cfg 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
  • 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 new tools/go.mod is added, meaning tools 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.