A Makefile for your Go project (2017)

Vincent Bernat

Up­date (2019-07)

Go 1.11 in­tro­duces mod­ules to man­age de­pen­den­cies with­out us­ing GOPATH. While some as­pects of the Makefile pre­sented here are still rel­e­vant (fetch­ing tools, run­ning tests), the main point be­comes moot. See the up­dated ver­sion of this guide.

My most loathed fea­ture of Go is the manda­tory use of GOPATH: I do not want to put my own code next to its de­pen­den­cies. Hope­fully, this is­sue is slowly start­ing to be ac­cepted by the main au­thors. In the mean­time, you can work­around this prob­lem with more opin­ion­ated tools (like gb) or by craft­ing your own Makefile.

For the lat­ter, you can have a look at Fil­ippo Val­sorda’s ex­am­ple or my own take which I de­scribe in more de­tail here. This is not meant to be a uni­ver­sal Makefile but a rel­a­tively short one with some bat­ter­ies in­cluded. It comes with a sim­ple “Hello World!” ap­plication.

Project structure#

For a stand­alone project, ven­dor­ing is a must-have1 as you can­not rely on your de­pen­den­cies to not in­tro­duce back­ward-in­com­pat­i­ble changes. Some pack­ages are us­ing ver­sioned URLs but most of them aren’t. There is cur­rently no stan­dard tool to han­dle ven­dor­ing. My per­sonal take is to ven­dor all de­pen­den­cies with Glide.2

It is a good prac­tice to split an ap­pli­ca­tion into dif­fer­ent pack­ages while the main one stays fairly small. In the hellogopher ex­am­ple, the CLI is han­dled in the cmd pack­age while the ap­pli­ca­tion logic for print­ing greet­ings is in the hello package:

.
├── cmd/
│   ├── hello.go
│   ├── root.go
│   └── version.go
├── glide.lock (generated)
├── glide.yaml
├── vendor/ (dependencies will go there)
├── hello/
│   ├── root.go
│   └── root_test.go
├── main.go
├── Makefile
└── README.md

Down the rabbit hole#

Let’s take a look at the var­i­ous “fea­tures” of the Makefile.

GOPATH handling#

Since all de­pen­den­cies are ven­dored, only our own project needs to be in the GOPATH:

PACKAGE  = hellogopher
GOPATH   = $(CURDIR)/.gopath
BASE     = $(GOPATH)/src/$(PACKAGE)

$(BASE):
    @mkdir -p $(dir $@)
    @ln -sf $(CURDIR) $@

The base im­port path is hellogopher, not github.com/vincentbernat/hellogopher: this short­ens im­ports and makes them eas­ily dis­tin­guish­able from im­ports of de­pen­dency pack­ages. How­ever, your ap­pli­ca­tion won’t be go get-able. This is a per­sonal choice and can be ad­justed with the $(PACKAGE) variable.

We just cre­ate a sym­link from .gopath/src/hellogopher to our root di­rec­tory. The GOPATH en­vi­ron­ment vari­able is au­to­mat­i­cally ex­ported to the shell com­mands of the recipes. Any tool should work fine af­ter chang­ing the cur­rent di­rec­tory to $(BASE). For ex­am­ple, this snip­pet builds the ex­e­cutable:

.PHONY: all
all: | $(BASE)
    cd $(BASE) && $(GO) build -o bin/$(PACKAGE) main.go

Vendoring dependencies#

Glide is a bit like Ruby’s Bundler. In glide.yaml, you spec­ify what pack­ages you need and the con­straints you want on them. Glide com­putes a glide.lock file con­tain­ing the ex­act ver­sions for each de­pen­dency (in­clud­ing re­cur­sive de­pen­den­cies) and down­load them in the vendor/ folder. I choose to check into the VCS both glide.yaml and glide.lock files. It’s also pos­si­ble to only check in the first one or to also check in the vendor/ di­rec­tory. A work-in-progress is cur­rently on­go­ing to pro­vide a stan­dard de­pen­dency man­age­ment tool with a sim­i­lar workflow.

We de­fine two rules:3

GLIDE = glide

glide.lock: glide.yaml | $(BASE)
    cd $(BASE) && $(GLIDE) update
    @touch $@
vendor: glide.lock | $(BASE)
    cd $(BASE) && $(GLIDE) --quiet install
    @ln -sf . vendor/src
    @touch $@

We use a vari­able to in­voke glide. This en­ables a user to eas­ily over­ride it (for ex­am­ple, with make GLIDE=$GOPATH/bin/glide).

Using third-party tools#

Most projects need some third-party tools. We can ei­ther ex­pect them to be al­ready in­stalled or com­pile them in our pri­vate GOPATH. For ex­am­ple, here is the lint rule:

BIN    = $(GOPATH)/bin
GOLINT = $(BIN)/golint

$(BIN)/golint: | $(BASE) # ❶
    go get github.com/golang/lint/golint

.PHONY: lint
lint: vendor | $(BASE) $(GOLINT) # ❷
    @cd $(BASE) && ret=0 && for pkg in $(PKGS); do \
        test -z "$$($(GOLINT) $$pkg | tee /dev/stderr)" || ret=1 ; \
     done ; exit $$ret

As for glide, we let the user a chance to over­ride which golint ex­e­cutable to use. By de­fault, it uses a pri­vate copy. But a user can use its own copy with make GOLINT=/usr/bin/golint.

In ❶, we have the recipe to build the pri­vate copy. We is­sue go get4 to down­load and build golint. In ❷, the lint rule ex­e­cutes golint on each pack­age con­tained in the $(PKGS) vari­able. We’ll ex­plain this vari­able in the next section.

Working with non-vendored packages only#

Some com­mands need to be pro­vided with a list of pack­ages. Be­cause we use a vendor/ di­rec­tory, the short­cut ./... is not what we ex­pect as we don’t want to run tests on our de­pen­den­cies.5 There­fore, we com­pose a list of pack­ages we care about:

PKGS = $(or $(PKG), $(shell cd $(BASE) && \
    env GOPATH=$(GOPATH) $(GO) list ./... | grep -v "^$(PACKAGE)/vendor/"))

If the user has pro­vided the $(PKG) vari­able, we use it. For ex­am­ple, if they want to lint only the cmd pack­age, they can in­voke make lint PKG=hellogopher/cmd which is more in­tu­itive than spec­i­fy­ing PKGS.

Oth­er­wise, we just ex­e­cute go list ./... but we re­move any­thing from the vendor/ di­rectory.

Up­date (2018-03)

Since Go 1.9, ./... no longer matches pack­ages in the vendor/ di­rectory.

Tests#

Here are some rules to run tests:

TIMEOUT = 20
TEST_TARGETS := test-default test-bench test-short test-verbose test-race
.PHONY: $(TEST_TARGETS) check test tests
test-bench:   ARGS=-run=__absolutelynothing__ -bench=.
test-short:   ARGS=-short
test-verbose: ARGS=-v
test-race:    ARGS=-race
$(TEST_TARGETS): test

check test tests: fmt lint vendor | $(BASE)
    @cd $(BASE) && $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(PKGS)

A user can in­voke tests in dif­fer­ent ways:

  • make test runs all tests;
  • make test TIMEOUT=10 runs all tests with a time­out of 10 seconds;
  • make test PKG=hellogopher/cmd only runs tests for the cmd package;
  • make test ARGS="-v -short" runs tests with the spec­i­fied ar­guments;
  • make test-race runs tests with race de­tec­tor enabled.

Tests coverage#

go test in­cludes a test cov­er­age tool. Un­for­tu­nately, it only han­dles one pack­age at a time and you have to ex­plic­itly list the pack­ages to be in­stru­mented, oth­er­wise the in­stru­men­ta­tion is lim­ited to the cur­rently tested pack­age. If you pro­vide too many pack­ages, the com­pi­la­tion time will sky­rocket. More­over, if you want an out­put com­pat­i­ble with Jenk­ins, you’ll need some ad­di­tional tools.

COVERAGE_MODE    = atomic
COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out
COVERAGE_XML     = $(COVERAGE_DIR)/coverage.xml
COVERAGE_HTML    = $(COVERAGE_DIR)/index.html

.PHONY: test-coverage test-coverage-tools
test-coverage-tools: | $(GOCOVMERGE) $(GOCOV) $(GOCOVXML) # ❸
test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -Iseconds)
test-coverage: fmt lint vendor test-coverage-tools | $(BASE)
    @mkdir -p $(COVERAGE_DIR)/coverage
    @cd $(BASE) && for pkg in $(PKGS); do \ # ❹
        $(GO) test \
            -coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $$pkg | \
                    grep '^$(PACKAGE)/' | grep -v '^$(PACKAGE)/vendor/' | \
                    tr '\n' ',')$$pkg \
            -covermode=$(COVERAGE_MODE) \
            -coverprofile="$(COVERAGE_DIR)/coverage/`echo $$pkg | tr "/" "-"`.cover" $$pkg ;\
     done
    @$(GOCOVMERGE) $(COVERAGE_DIR)/coverage/*.cover > $(COVERAGE_PROFILE)
    @$(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML)
    @$(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML)

First, we de­fine some vari­ables to let the user over­ride them. We also re­quire the fol­low­ing tools (in ❸):

  • gocovmerge merges pro­files from dif­fer­ent runs into a single one;
  • gocov-xml con­verts a cov­er­age pro­file to the Cober­tura format;
  • gocov is needed to con­vert a cov­er­age pro­file to a for­mat han­dled by gocov-xml.

The rules to build these tools are sim­i­lar to the rule for golint de­scribed a few sec­tions ago.

In ❹, for each pack­age to test, we run go test with the -coverprofile ar­gu­ment. We also ex­plic­itly pro­vide the list of pack­ages to in­stru­ment to -coverpkg by us­ing go list to get a list of de­pen­den­cies for the tested pack­age and keep­ing only our own.

Final result#

While the main goal of us­ing a Makefile was to work around GOPATH, it is also a good place to hide the com­plex­ity of some op­er­a­tions, no­tably around test coverage.

The ex­cerpts pro­vided in this post are a bit sim­pli­fied. Have a look at the fi­nal re­sult for more perks!