A Makefile For Go Projects
Useful tasks for Makefiles in Go projects.
Let’s start with a simple task, create a Makefile at the root of your project with the contents:
dev:
air .
You can now run make dev
and it will execute the command air .
.
Air is a tool I use for hot-reloading during development.
Makefiles are supposed to deal with files, in our case, we are using it as a task runner, so, we will add the .PHONY
target to be explicit about that fact:
.PHONY: dev
dev:
@echo "Starting web server in development mode"
ENV=development air .
The @
at the beginning of the echo command means “do not print this line”, which would be redundant for echo.
I have added an environment variable ENV
, it’s important that you do this in the same line as the command that the variable is supposed to affect (air
in this case). If you set the environment variable in one line and run the command in another one, it won’t work.
This won’t work as expected:
.PHONY: dev
dev:
@echo "Starting web server in development mode"
export ENV=development
air .
If you run make
with no arguments, it will run the first task in the Makefile. So, I think it’s a good idea to put a help task as the first one, which will print a small description of every task available:
## help: print this help message.
.PHONY: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
For any task that you wish to show in the help message, you just need to write a comment with the format ## <task>: <message>
.
Let’s add a help message to our dev task:
## dev: run with hot-reloading.
.PHONY: dev
dev:
@echo "Starting web server in development mode"
export ENV=development
air .
If you now run make
or make help
, you will get:
Usage:
help print this help message.
dev run with hot-reloading.
Usually, in my Go projects I would use tools that are not part of the application itself, they are not listed in go.mod so to say. For example, here we are using air for hot-reloading, and if I were to work with a database, I would use something like tern to handle migrations.
I like having one task that consolidates these dependencies, so that it’s clear for developers joining the project what tools are used, and they can install them with a single command:
## deps: install external dependencies not used in source code
.PHONY: deps
deps:
@echo 'Installing `air` for hot-reloading'
go install github.com/cosmtrek/air@latest
Since this is going to install things on users’ systems, I think it would be nice to ask for confirmation before proceeding. We can have a little confirm helper that we can attach to any task we wish:
.PHONY: confirm
confirm:
@echo -n 'Are you sure? [y/N]' && read ans && [ $${ans:-N} = y ]
Notice the lack of help message, that’s because this is a task for internal use only.
To ask for confirmation, we simply need to add the confirm task. Let’s add it to deps:
## deps: install external dependencies not used in source code
.PHONY: deps
deps: confirm
@echo 'Installing `air` for hot-reloading'
go install github.com/cosmtrek/air@latest
If you run make deps
, you will get:
Are you sure? [y/N]
This wouldn’t be complete without adding a task for tests. In addition to tests, we will also tidy and verify dependencies, fmt and vet the code:
## audit: tidy dependencies, format, vet and test.
.PHONY: audit
audit:
@echo 'Tidying and verifying module dependencies...'
go mod tidy
go mod verify
@echo 'Formatting code...'
go fmt ./...
@echo 'Vetting code...'
go vet ./...
@echo 'Running tests...'
ENV=testing go test -race -vet=off ./...
Another thing that might come in handy in a makefile is to read an environemt variable and use a default if it is not present. We can achieve that with the following syntax:
SOME_VAR ?= 'i_am_a_default'
We can then use it in any task:
SOME_VAR ?= 'i_am_a_default'
.PHONY: example
example:
@echo 'Value of SOME_VAR is: ${SOME_VAR}'
If you run make example
, you will get Value of SOME_VAR is: i_am_a_default
.
If you run SOME_VAR=injected make example
, you will get Value of SOME_VAR is: injected
.
Some other useful tasks to have in a makefile are related to dependency management. govulncheck ./...
checks the project dependencies for vulnerabilities. As a result of a vulnerability check, it is common to need to upgrade dependencies, so it comes in handy to have a task to run go get -t -u ./...
; or go get -t -u=patch ./...
if you just want to upgrade to the latest patch version.
I would normally have one or more build tasks, maybe one to build for the current arch and another one to build for all targets. If I’m working with a database, I would also have tasks to deal with creating, applying and reverting migrations. But these are more project-specific so I won’t include them here.
To sum up, this is the entire Makefile:
## help: print this help message.
.PHONY: help
help:
@echo 'usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
.PHONY: confirm
confirm:
@echo -n 'Are you sure? [y/N]' && read ans && [ $${ans:-N} = y ]
## audit: tidy dependencies, format, vet and test.
.PHONY: audit
audit:
@echo 'Tidying and verifying module dependencies...'
go mod tidy
go mod verify
@echo 'Formatting code...'
go fmt ./...
@echo 'Vetting code...'
go vet ./...
@echo 'Checking vulnerabilities...'
govulncheck ./...
@echo 'Running tests...'
ENV=testing go test -race -vet=off ./...
## dev: run with hot-reloading.
.PHONY: dev
dev:
ENV=development air .
## deps/upgrade/all: upgrade all dependencies
.PHONY: deps/upgrade/all
deps/upgrade/all:
@echo 'Upgrading dependencies to latest versions...'
go get -t -u ./...
## deps/upgrade/patch: upgrade dependencies to latest patch version
.PHONY: deps/upgrade/patch
deps/upgrade:
@echo 'Upgrading dependencies to latest patch versions...'
go get -t -u=patch ./...
## deps/ext: install external dependencies not used in source code
.PHONY: deps/ext
deps/ext: confirm
@echo 'Installing `air` for hot-reloading'
go install github.com/cosmtrek/air@latest
@echo 'Installing `tern` for db migrations'
go install github.com/jackc/tern/v2@latest
## vuln: check for vulnerabilities
.PHONY: vuln
vuln:
govulncheck ./...
Some of this I have stolen from two great books by Alex Edwards: