Omitting dev dependencies in Go binaries
Redowan Delowar
January 21, 2024
As of now, unlike Python or NodeJS, Go doesn't allow you to specify your development
dependencies separately from those of the application. However, I like to specify the dev
dependencies explicitly for better reproducibility.
While working on [link-patrol, a CLI for checking dead URLs] in Markdown files, I came
across this neat convention: you can specify dev dependencies in a tools.go file and then
exclude them while building the binary using a build tag.
Here's how it works. Let's say our project foo currently has the following structure:
The main.go file contains a simple hello-world function that uses a 3rd party dependency
just to make a point:
Here, Neo-cowsay is our app dependency. To initialize the project, we run the following
commands serially:
Now, let's say we want to add the following dev dependencies: [golangci-lint] to lint the
project in the CI and [gofumpt] as a stricter gofmt. Since we don't import these tools
directly anywhere, they aren't tracked by the build toolchain.
But we can leverage the following workflow:
- Place a tools.go file in the root directory.
- Import the dev dependencies in that file.
- Run go mod tidy to track both app and dev dependencies via go.mod and go.sum.
- Specify a build tag in tools.go to exclude the dev dependencies from the binary.
In this case, tools.go looks as follows:
Above, we're importing the dev dependencies and assigning them to underscores since we won't
be using them directly. However, now if you run go mod tidy, Go toolchain will track the
dependencies via the go.mod and go.sum files. You can inspect the dependencies in
go.mod:
Although we're tracking the dev dependencies along with the app ones, the build tag
// go:build tools at the beginning of tools.go file will instruct the build toolchain to
ignore them while creating the binary.
From the root directory of foo, you can build the project by running:
This will create a binary called main in the root directory. To ensure that the binary
doesn't contain the dev dependencies, run:
This won't return anything if the dev dependencies aren't packed into the binary.
But if you do that for the app dependency, it'll print the artifacts:
This prints:
For some weird reason, if you want to include the dev dependencies in your binary, you can
pass the tools tag while building the binary:
However, this will most likely fail if any of your dev dependencies aren't importable.
Here's an example of [Kubernetes's tools.go pattern] in the wild.
While it works, I'd still prefer to have a proper solution instead of a hack. Fin!
[link-patrol, a CLI for checking dead URLs]:
https://github.com/rednafi/link-patrol
[golangci-lint]:
https://github.com/golangci/golangci-lint
[gofumpt]:
https://github.com/mvdan/gofumpt
[kubernetes's tools.go pattern]:
https://github.com/kubernetes/kubernetes/blob/master/hack/tools/tools.go
Discussion in the ATmosphere