
#Makefiles for Go Developers
In this tutorial we are going to look at how you can use Makefiles to automate the most common tasks in a Go project — building, running, and cross-compiling your application — all from a single make command. By the end you’ll have a working Makefile you can drop into any Go project and extend from there.
What are Makefiles?
Makefiles are an automation tool built around the make command-line utility. You define named targets, each containing a shell script to run, and then invoke them with make <target>. They’re not Go-specific — you’ll find them in C, Python, and Rust projects too — but they’re particularly common in the Go ecosystem because Go projects often need a short sequence of commands (format, vet, build, test) chained together before a binary is ready.
Browse any large Go repository on GitHub and you’ll almost certainly find a Makefile at the root.
A Simple Example
Let’s start with the smallest possible Makefile to understand the structure before adding Go-specific targets.
Create a new directory to work in and add a file called Makefile (no extension):
hello:
echo "Hello"
Run it:
$ make hello
echo "Hello"
Hello
A target is just a name followed by a colon. The lines below it — indented with a tab, not spaces — are the shell commands that run when you call that target. Make prints each command before executing it, which is why you see echo "Hello" before Hello.
Building a Simple Go App
Now let’s bring in a Go application. Add a main.go alongside your Makefile:
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
Then extend your Makefile with build and run targets:
hello:
echo "Hello"
build:
go build -o bin/main main.go
run:
go run main.go
Try each one:
$ make build
go build -o bin/main main.go
$ make run
go run main.go
Hello
The build target compiles the application to bin/main — the -o flag controls the output path. Keeping compiled binaries under bin/ is a common Go convention that makes it easy to add bin/ to your .gitignore. The run target skips the compilation step entirely and invokes go run, which is faster during active development when you don’t need a distributable binary.
Cross-Compiling for Multiple Platforms
One of the strongest reasons to use a Makefile in a Go project is cross-compilation. Go’s toolchain can produce binaries for any OS and architecture by setting two environment variables — GOOS and GOARCH — but typing that by hand for every combination is tedious. A Makefile target captures it once:
compile:
echo "Compiling for every OS and Platform"
GOOS=linux GOARCH=arm go build -o bin/main-linux-arm main.go
GOOS=linux GOARCH=arm64 go build -o bin/main-linux-arm64 main.go
GOOS=freebsd GOARCH=386 go build -o bin/main-freebsd-386 main.go
$ make compile
echo "Compiling for every OS and Platform"
Compiling for every OS and Platform
GOOS=linux GOARCH=arm go build -o bin/main-linux-arm main.go
GOOS=linux GOARCH=arm64 go build -o bin/main-linux-arm64 main.go
GOOS=freebsd GOARCH=386 go build -o bin/main-freebsd-386 main.go
After running this, your bin/ directory will contain three separate binaries ready to deploy to their respective platforms. GOOS and GOARCH are set inline so they only apply to that single command — they don’t leak into your shell environment.
Layering Commands with Dependencies
As a project grows, you end up with a sequence of tasks that always run together: maybe you always want to run tests before building, or format before linting. Makefiles handle this with dependencies — a target can list other targets it depends on, and Make will run those first.
Here’s a full Makefile that pulls everything together with an all target:
hello:
echo "Hello"
build:
go build -o bin/main main.go
run:
go run main.go
compile:
echo "Compiling for every OS and Platform"
GOOS=linux GOARCH=arm go build -o bin/main-linux-arm main.go
GOOS=linux GOARCH=arm64 go build -o bin/main-linux-arm64 main.go
GOOS=freebsd GOARCH=386 go build -o bin/main-freebsd-386 main.go
all: hello build
$ make all
echo "Hello"
Hello
go build -o bin/main main.go
Listing hello and build after the colon on the all line tells Make to run those targets in order before running any commands in all itself. This pattern is how real projects chain formatting, linting, testing, and building into a single make all or make ci command.
Frequently Asked Questions
What is a Makefile and why do Go developers use it?
A Makefile is a configuration file for the make build tool that lets you define named shell-script targets. Go developers use Makefiles to bundle common commands — building, testing, linting, cross-compiling — so the whole team runs the same commands the same way, regardless of IDE or shell.
Do I need to install anything to use make in Go projects?
make is pre-installed on macOS and most Linux distributions. On Windows you can get it via WSL, Git Bash, or Chocolatey (choco install make). Go itself has no dependency on make.
Why does make fail with “missing separator” errors?
Makefiles require tabs (not spaces) to indent the commands under a target. Most editors default to spaces and will silently produce broken Makefiles. Configure your editor to use literal tabs in files named Makefile, or use :set noexpandtab in Vim.
How do I pass variables into a Makefile target?
Define a variable at the top with VAR=default and reference it with $(VAR). You can override it at call time: make build VERSION=1.2.3. This is useful for injecting version strings into go build -ldflags.
Can I use a Makefile to run go test and go vet?
Yes — and this is one of the most common uses. A typical Go Makefile includes targets like test: go test ./... and vet: go vet ./..., often chained as dependencies on a check or ci target.
What’s the difference between make run and just running the binary directly?
make run executes go run main.go, which compiles and runs in one step without producing a binary on disk. Running the binary directly (e.g. ./bin/main) executes a previously compiled artifact. Use make run during development for speed; use the binary for deployment or benchmarking.
What’s Next
- Building a REST API in Go — apply these Makefile patterns to a real multi-package project
- Go Modules Tutorial — manage dependencies alongside your build automation
- Writing a Go Web Server — a natural next project to wire up with a
Makefile
Continue Learning
Structured Logging in Go with log/slog - The Complete Guide
Learn structured logging in Go with the standard library log/slog package - handlers, levels, context, custom handlers, and why it replaces logrus, zap and zerolog.
An Introduction to Go Closures - Tutorial
Learn how closures work in Go with simple, practical examples. Understand lexical scoping and how closures capture and maintain their own state.
Go Interfaces Tutorial
Learn how Go interfaces work: implicit satisfaction, defining contracts, and writing flexible, testable code without inheritance.
Reading And Writing To Files in Go
Learn how to read and write files in Go using the os package — covering os.ReadFile, os.WriteFile, appending to existing files, and file permissions.