Improve your DX in Go with Makefiles 🐹
¿Hablas español? Ver en español
Something especially important as developers is to enjoy our work and to help the whole team enjoy it too. That is why the term DX (Developer Experience) exists and in this Post, we will delve into a straightforward way to improve the DX of our projects in Go using the well-known Makefiles (created in the 70’s 🤯).
What is a Makefile?
A Makefile is a tool to simplify or organize the code for compilation. A Makefile is a set of commands with variable names and targets for creating or removing binary and object files. A Makefile can be used to compile code in languages like C or C++, or to provide commands to automate common tasks. A Makefile helps decide which parts of a large program need to be recompiled.
How will a Makefile improve our DX in Go?
If you’ve had a chance to work on the Frontend side, you’ll know that there is a list of commands within Package.json that you can customize to perform specific tasks, such as running tests, applying a linter, building the app, or running it. Well, that will do our Makefile for us in Go.
Our Go Makefile will help us organize our various commands, so we don’t have to specifically teach each new developer in the project lengthy commands to generate mocks, for example, or to get a specific code coverage format.
Examples of commands that can be added to our Makefile
First, we will start from the fact that in our Go project we have a scripts folder. Once this is clear, we will create a Makefile at the root of it and there we will add our different commands that we will use.
Our project would look something like this (I’ll leave the folders empty so we don’t get distracted. This template is from my Post 2023 project template).
Our makefile would look like this: we would define the different scripts we want to use with the make keyword. In this case, we will do the Build, Test, Run and Generation of Mocks process.
- We define our commands to use.
First, we define the tools that we will use in variables that remind us of the actions that we normally perform with the go CLI. We do this so that if our Makefile grows and then one of the go tools changes names, we can easily replace it without modifying each command individually.
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOTOOL=$(GOCMD) tool
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOINST=$(GOCMD) install
2. We define our Build command
A common process in Go is building our app, and when new developers come along, remembering the build parameters can be a bit tedious. So, we will use the Makefile to define our project build command.
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOTOOL=$(GOCMD) tool
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOINST=$(GOCMD) install
#Binary Name
BINARY_NAME=main
# Build
build:
@$(GOBUILD) -o $(BINARY_NAME) ./cmd/http
@echo "📦 Build Done"
3. We add our Tests command
Another common process in Go is to run the tests of our application to verify its correct operation and detect possible errors. To do this, we can use the go test command, which finds and runs the files ending in _test.go in our project. However, this command can have multiple options and arguments that can complicate its use. For example, we may want to specify the level of coverage of the tests, the output format, the packages to test or the flags to pass to the test runner. To simplify this process, we can define our tests command in the Makefile, using the variables we defined earlier and adding any options we need.
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOTOOL=$(GOCMD) tool
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOINST=$(GOCMD) install
#Binary Name
BINARY_NAME=main
# Build
build:
@$(GOBUILD) -o $(BINARY_NAME) ./cmd/http
@echo "📦 Build Done"
# Test
test:
@$(GOTEST) -v ./...
@echo "🧪 Test Completed"
4. We now add our Run command to our binary.
Once we’ve built our binary with the make build command, we can run it directly from the terminal with ./main. However, it may be convenient to define a command in the Makefile to run our binary more easily and consistently. To do this, we can use the run command and specify the name of the binary we want to run. Thus, we can start our application just by typing make run in the terminal.
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOTOOL=$(GOCMD) tool
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOINST=$(GOCMD) install
#Binary Name
BINARY_NAME=main
# Build
build:
@$(GOBUILD) -o $(BINARY_NAME) ./cmd/http
@echo "📦 Build Done"
# Test
test:
@$(GOTEST) -v ./...
@echo "🧪 Test Completed"
# Run
run:
@echo "🚀 Running App"
@./$(BINARY_NAME)
5. We can also run more complex scripts using the scripts folder.
Sometimes, we may need to run scripts that perform more complex tasks than what we can define in the Makefile. For example, we may want to generate mocks for our tests, format our code, or generate documentation. To do this, we can use the scripts folder that we have created in our project and store the scripts that we want to execute there. Then we can define commands in the Makefile that call those scripts using the sh command. Thus, we can execute our scripts just by typing make script-name in the terminal.
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOTOOL=$(GOCMD) tool
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOINST=$(GOCMD) install
#Binary Name
BINARY_NAME=main
# Build
build:
@$(GOBUILD) -o $(BINARY_NAME) ./cmd/http
@echo "📦 Build Done"
# Test
test:
@$(GOTEST) -v ./...
@echo "🧪 Test Completed"
# Run
run:
@echo "🚀 Running App"
@./$(BINARY_NAME)
# Generate Mocks
generate-mocks:
@$(GOINST) github.com/golang/mock/mockgen@v1.6.0
@./scripts/generate-mocks.sh
6. We can also call other commands from our make file
Let’s say we want to have a dev command that builds and runs the app for us at the same time. To do this, we will create our dev command with a dependency on the build command. This means that before running the dev command, the build command will be run to make sure we have the up-to-date binary. The dev command will then run the binary using the name we have assigned to it. Thus, we can start our app just by typing make dev in the terminal.
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOTOOL=$(GOCMD) tool
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOINST=$(GOCMD) install
#Binary Name
BINARY_NAME=main
# Build
build:
@$(GOBUILD) -o $(BINARY_NAME) ./cmd/http
@echo "📦 Build Done"
# Test
test:
@$(GOTEST) -v ./...
@echo "🧪 Test Completed"
# Run
run:
@echo "🚀 Running App"
@./$(BINARY_NAME)
# Generate Mocks
generate-mocks:
@$(GOINST) github.com/golang/mock/mockgen@v1.6.0
@./scripts/generate-mocks.sh
# Dev
dev:build
@echo "🚀 Running App"
@./$(BINARY_NAME)
Using our commands
Using our Makefile commands is amazingly simple: we’ll use the make keyword followed by the name of the command we want to perform.
We will see here some examples of how to use it:
- To build our application we will use the make build command.
- To run our tests, we will use the make test command.
- To run our program in dev mode `make dev`
Conclusion
In this article, we’ve seen how Makefiles can improve our Go development experience by simplifying and organizing the commands we use to build, run, and test our application. We’ve learned the basic syntax of a Makefile, how to define variables, targets, and dependencies, and how to run more complex scripts from the Makefile. We’ve also seen some examples of useful commands that we can use in our Go projects, such as build, test, run, and dev. I hope this article has been useful to you and that you are encouraged to use Makefiles in your Go projects. Until next time!