Ultimate Makefile for Golang

10 Jun 2024 ⏱️ 6 min
Ultimate Makefile for Golang

Developing and testing extensive codebases can be a time-consuming, error-prone, and repetitive endeavor. While Golang offers support for multi-platform builds, the process often necessitates executing multiple commands to generate binaries for various platforms, resulting in additional time consumption and repetition. Moreover, most projects entail dependencies that must be installed before compiling the binary, and it’s imperative to conduct thorough testing, as well as ensure code quality using linters and code coverage tools.

Enter the utility tool Make, renowned for its capacity to automate tasks. By streamlining development and automating repetitive procedures with a single command, Make significantly enhances efficiency. It proves invaluable for testing, building, cleaning, and installing Go projects. In this tutorial, we will delve into harnessing the power of Make and makefiles to automate these tedious and repetitive tasks associated with Golang development. You will gain insights into how to utilize Make and a Makefile to seamlessly build, clean, and test a sample Go project.

Leveraging Makefiles can revolutionize this process, ensuring that your project is built efficiently and consistently, while saving you valuable time and effort.


What is a Makefile?

A Makefile is a simple text file that contains instructions for building a software project. It is used by the make utility, a popular build automation tool available on Unix-based systems, including Linux and macOS. The Makefile specifies the project’s dependencies, build rules, and targets, allowing the make utility to compile and link your source files into an executable program.

Adding a Makefile To Your Project

To start using make commands, you first need to create a Makefile in the root directory of your project. Let’s create a simple hello world project with a Makefile in it.

package main

import "fmt"

func main() {
 fmt.Println("hello world")
}

To run this project, you would normally need to build the project and run the binary:

go build main.go

If you want a different binary name and also want to create a build for a specific OS, you can specify this during the build:

GOARCH=amd64 GOOS=darwin go build -o hello-world main.go

If you want to run, each time you’ll have to execute this command.

go run hello-world

The above commands can be simplified using Makefile. You can specify rules to a specific command and run a simple make command. You would not need to remember the commands and the flags or environment variables needed for executing it.

Basic Makefile

APP_EXECUTABLE=hello-world

build:
 GOARCH=amd64 GOOS=darwin go build -o ${APP_EXECUTABLE}-darwin main.go
 GOARCH=amd64 GOOS=linux go build -o ${APP_EXECUTABLE}-linux main.go
 GOARCH=amd64 GOOS=windows go build -o ${APP_EXECUTABLE}-windows main.go

run: build
 ./${APP_EXECUTABLE}

clean:
 go clean
 rm ${APP_EXECUTABLE}-darwin
 rm ${APP_EXECUTABLE}-linux
 rm ${APP_EXECUTABLE}-windows

Now with these simple commands, you can build and run the Go project:

make run

Finally, you can run the clean command for the cleanup of binaries:

make clean

These commands are very handy and help to streamline the development process. Now all of your team members can use the same command. This reduces inconsistency and helps to eliminate project build-related errors that can arise with inconsistent manual commands.

Ultimate Makefile

Our makefile has very basic commands right now. We can do a lot more while working with Makefile commands.

export GO111MODULE=on
# update app name. this is the name of binary
APP=myapp
APP_EXECUTABLE="./out/$(APP)"
ALL_PACKAGES=$(shell go list ./... | grep -v /vendor)
SHELL := /bin/bash # Use bash syntax

# Optional if you need DB and migration commands
# DB_HOST=$(shell cat config/application.yml | grep -m 1 -i HOST | cut -d ":" -f2)
# DB_NAME=$(shell cat config/application.yml | grep -w -i NAME  | cut -d ":" -f2)
# DB_USER=$(shell cat config/application.yml | grep -i USERNAME | cut -d ":" -f2)

# Optional colors to beautify output
GREEN  := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE  := $(shell tput -Txterm setaf 7)
CYAN   := $(shell tput -Txterm setaf 6)
RESET  := $(shell tput -Txterm sgr0)

## Quality
check-quality: ## runs code quality checks
	make lint
	make fmt
	make vet

# Append || true below if blocking local developement
lint: ## go linting. Update and use specific lint tool and options
	golangci-lint run --enable-all

vet: ## go vet
	go vet ./...

fmt: ## runs go formatter
	go fmt ./...

tidy: ## runs tidy to fix go.mod dependencies
	go mod tidy

## Test
test: ## runs tests and create generates coverage report
	make tidy
	make vendor
	go test -v -timeout 10m ./... -coverprofile=coverage.out -json > report.json

coverage: ## displays test coverage report in html mode
	make test
	go tool cover -html=coverage.out

## Build
build: ## build the go application
	mkdir -p out/
	go build -o $(APP_EXECUTABLE)
	@echo "Build passed"

run: ## runs the go binary. use additional options if required.
	make build
	chmod +x $(APP_EXECUTABLE)
	$(APP_EXECUTABLE)

clean: ## cleans binary and other generated files
	go clean
	rm -rf out/
	rm -f coverage*.out

vendor: ## all packages required to support builds and tests in the /vendor directory
	go mod vendor


wire: ## for wiring dependencies (update if using some other DI tool)
	wire ./...

# [Optional] mock generation via go generate
# generate_mocks:
# 	go generate -x `go list ./... | grep - v wire`

# [Optional] Database commands
## Database
migrate: build
	${APP_EXECUTABLE} migrate --config=config/application.test.yml

rollback: build
	${APP_EXECUTABLE} migrate --config=config/application.test.yml

.PHONY: all test build vendor
## All
all: ## runs setup, quality checks and builds
	make check-quality
	make test
	make build

.PHONY: help
## Help
help: ## Show this help.
	@echo ''
	@echo 'Usage:'
	@echo '  ${YELLOW}make${RESET} ${GREEN}<target>${RESET}'
	@echo ''
	@echo 'Targets:'
	@awk 'BEGIN {FS = ":.*?## "} { \
		if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf "    ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
		else if (/^## .*$$/) {printf "  ${CYAN}%s${RESET}\n", substr($$1,4)} \
		}' $(MAKEFILE_LIST)

Benefits of Using Makefiles

  1. Automation: Makefiles allow you to automate tedious and repetitive tasks, such as compiling code, running tests, and cleaning up build artifacts. This automation not only saves time but also reduces the risk of human error, ensuring that these tasks are performed consistently every time. With a Makefile, you can simply run the ‘make’ command to build your entire project without having to remember or type out lengthy compilation commands.
  2. Consistency: Makefiles ensure that your project is built consistently, regardless of who is building it or on which system it is being built. It ensures common standards are followed across projects. Makefiles also provide a standardized way to define and execute build processes, which helps maintain consistency across different development and production environments.
  3. Customization: Makefiles can be customized to include various build configurations, such as debug and release builds, and to support cross-compilation for different platforms.

Books to learn Golang

Do explore articles on Golang and System Design. You’ll learn something new 💡

Liked the article? Consider supporting me ☕️


I hope you learned something new. Feel free to suggest improvements ✔️

Follow me on Twitter for updates and resources. Let’s connect!

Keep exploring 🔎 Keep learning 🚀

Liked the content? Do support :)

Paypal - Mohit Khare
Buy me a coffee