Using Makefiles with Go

Nate Catelli
Using make to wrap golang with extra functionality.
December 11, 2019


One of my favorite features of golang is its simple toolchain for builds. However at times, I’ve wished that I could easily add tasks to a build step. Using GNU Make, I’ve found that I can quickly and easily wrap the go toolchain in a consistent way that leaves plenty of room for customization.

Wrapping Common Go Commands:

Primarily, I’ve been able to get away with 1:1 mapping of many of the go tool chain directly behind corresponding make commands.

The fmt command is mostly a copy paste of the corresponding go command. However, by leveraging some core functionality within make, we are able to begin defining dependency chains in other steps that allow us to insure that code is formatted, linted and tested prior to builds.


build: | fmt lint test
	go build

build-docker: | fmt lint test
	docker build -t ${IMG_NAME}:latest .

	go test -race -cover ./...

	test -z $(shell go fmt ./...)

	@type docker >/dev/null 2>&1 && \
	docker rmi -f ${IMG_NAME}:latest || \

clean: clean-docker
	@rm -f ${APP_NAME} || true

	golint -set_exit_status ./...

Leveraging go modules

By leveraging go modules, we can also insure that each build will have the required dependencies for our toolchain, an example being the golint command as can be seen in the below example go.mod file.


require ( v0.0.0-20191125180803-fdd1cda4f05f // indirect

go 1.13

Use With cGo:

Though simply wrapping the go toolchain appears to add very little value while adding additional complexity, we begin to see greater benefit when dealing with additional external C libraries. My first use of makefiles with go was while working with libfreeipmi. At the time, I was attempting to implement golang bindings for a limited subset of libfreeipmi which required building the shared objects for libfreeipmi from source. Adding this build process to the makefile simplified the building of the library and was easily defined by adding a few extra blocks:

BUILDOPTS=--ldflags '-extldflags "-static"'
DEPSCONFOPTS=--enable-static --without-encryption

build: | test
  go build $(BUILDOPTS) $(PKG)

test: | fmt deps
  go test $(BUILDOPTS) $(PKG) -v

  go fmt $(PKG)

doc: | fmt
  godoc $(PKG) > $(GOPATH)/src/$(PKG)/

  cd libs/freeipmi; \
  ./ && \
  ./configure $(DEPSCONFOPTS) && \

  cd libs/freeipmi; \
  make clean

Leveraging Docker Environments in Makefiles:

If you are not using cGo, you can still benefit from the Makefiles abstraction by wrapping a docker build environment. I’ve included an example of a Makefile from our pasteclick project that wraps a small docker environment with libmagic installed.


build: | test
  docker run -it --rm -u root -v `pwd`:/go/src/$(PKG) $(GOENV) go build $(PKG)

  docker run -it --rm -u root -v `pwd`:/go/src/$(PKG) $(GOENV) go fmt $(PKG)

test: | fmt
  docker run -it --rm -u root -v `pwd`:/go/src/$(PKG) $(GOENV) go test $(PKG)

Leveraging a container and make, one is able to provide a consistent build process in a build environment that is repeatable across platforms.


While the go toolchain is sufficient for purely go packages, leveraging simple makefiles to augment this toolchain with additional tasks is a simple and viable option for keeping your build processes down to a few concise commands.