Something that Go does very well is multi-platform support. You can build a binary for just about any system without much hassle. On a single build machine, you can build binaries for Windows, macOS, and many flavors of Linux. All that is required is to change the GOOS and GOARCH env variables to the desired OS and architecture. I made a Makefile to take advantage of this.

First I started with a few variables. EXECUTABLE is the name of the program to build. WINDOWS, LINUX, and DARWIN are the actual names of the binaries to be created, based off of EXECUTABLE. VERSION is a version string derived from the latest git tag and commit hash, something like v1.1.1-8-g99740b5.

VERSION=$(shell git describe --tags --always --long --dirty)

Next up, I created the actual build targets. The first three targets (windows|linux|darwin) are just for convenience so I can easily type make linux and build for Linux. Setting GOOS and GOARCH before calling go build causes the compiler to build for the specified environment. Setting main.version=$(VERSION) will set a version variable in the main package to the value of VERSION. This is extremely useful for auto-versioning binaries. The other flags aren’t as important to discuss here, but are for verbosity and optimization.

windows: $(WINDOWS) ## Build for Windows

linux: $(LINUX) ## Build for Linux

darwin: $(DARWIN) ## Build for Darwin (macOS)

	env GOOS=windows GOARCH=amd64 go build -i -v -o $(WINDOWS) -ldflags="-s -w -X main.version=$(VERSION)"  ./cmd/service/main.go

	env GOOS=linux GOARCH=amd64 go build -i -v -o $(LINUX) -ldflags="-s -w -X main.version=$(VERSION)"  ./cmd/service/main.go

	env GOOS=darwin GOARCH=amd64 go build -i -v -o $(DARWIN) -ldflags="-s -w -X main.version=$(VERSION)"  ./cmd/service/main.go

Next step is to add a target to build for all three operating systems. I like echoing the version so I can easily see what I’ve built.

build: windows linux darwin ## Build binaries
	@echo version: $(VERSION)

Next up, add some standard targets. test to run any unit tests. clean to remove old binaries. all is the default for when you just run make and I want it to run the tests, then build everything.

all: test build ## Build and run tests

test: ## Run unit tests
	go test ./...

clean: ## Remove previous build
	rm -f $(WINDOWS) $(LINUX) $(DARWIN)

.phony is used to force the specified targets to always execute, regardless of whether the output already exists.

.PHONY: all test clean

And finally, some grep magic to create a help target. I found this code around the internet somewhere and I’ve been using it ever since. It nicely formats and outputs the comments in the Makefile.

help: ## Display available commands
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

I use modified versions of this Makefile for all my various Go projects. Being able to easily build native binaries for almost any platform is a huge strength. The source for this example can be found here:

Gopher artwork by Ashley McNamara