Quick tip: A time-saving Makefile for your Go projects
Whenever I start a new Go project, one of the first things I do is create a Makefile in the root of my project directory.
This Makefile serves two purposes. The first is to automate common admin tasks (like running tests, checking for vulnerabilities, pushing changes to a remote repository, and deploying to production), and the second is to provide short aliases for Go commands that are long or difficult to remember. I find that it's a simple way to save time and mental overhead, as well as helping me to catch potential problems early and keep my codebases in good shape.
While the exact contents of the Makefile changes from project to project, in this post, I want to share the boilerplate that I'm currently using as a starting point. It's generic enough that you should be able to use it as-is for almost all projects.
Makefile is organized into several sections, each with its own set of targets:
help: Prints a help message for the Makefile, including a list of available targets and their descriptions.
confirm: Prompts the user to confirm an action with a "y/N" prompt.
no-dirty: Checks that there there are no uncommitted changes in the tracked files in the current git repository.
2. QUALITY CONTROL
tidy: Formats the code using
go fmtand updates the dependencies using
go mod tidy.
audit: Runs quality control checks on the codebase, including verifying the dependencies with
go mod verify, running running static analysis with
staticcheck, checking for vulnerabilities using
govulncheck, and running all tests. Note that it uses
go runto execute the latest versions of the remote
govulncheckpackages, meaning that you don't need to install these tools first. I've written more about this pattern in a previous post.
test: Runs all tests. Note that we enable the race detector and embed build info in the test binary.
test/cover: Runs all tests and outputs a coverage report in HTML format.
build: Builds the package at
MAIN_PACKAGE_PATHand outputs a binary at
run: Calls the
buildtarget and then runs the binary. Note that my main reason for not using
go runhere is that
go rundoesn't embed build info in the binary.
run/live: Use the
airtool to run the application with live reloading enabled. When changes are made to any files with the specified extensions, the application is rebuilt and the binary is re-run.
- Depending on the project I often add more to this section, such as targets for connecting to a development database instance and managing SQL migrations. Here's an example.
push: Push changes to the remote Git repository. This automatically runs the
auditquality-control checks first, and
no-dirtyto check that there are no uncommitted changes to tracked files.
production/deploy: Builds the a binary for linux/amd64 architecture, compress it using
upx, and then run any deployment steps. Note that this target asks for y/N confirmation before anything is executed, and also runs the
- Depending on the project I often add more to this section too. For example, a
staging/deployrule for deploying to a staging server,
production/connectfor SSHing into a production server,
production/logfor viewing production logs,
production/dbfor connecting to the production database, and
production/upgradefor updating and upgrading software on a production server.
Each of these targets can be executed by running
make followed by the target name in your terminal. For example:
If you run
make help (or the naked
make command without specifiying a target) then you'll get a description of the available targets.