My most loathed feature of Go is the mandatory use of GOPATH : I do not want to put my own code next to its dependencies . Hopefully, this issue is slowly starting to be accepted by the main authors. In the meantime, you can workaround this problem with more opinionated tools (like gb ) or by crafting your own Makefile .
For the later, you can have a look at Filippo Valsorda’s example or my own take which I describe in more details here. This is not meant to be an universal Makefile but a relatively short one with some batteries included. It comes with a simple “Hello World!”application.
Project structureFor a standalone project, vendoring is a must-haveas you cannot rely on your dependencies to not introduce backward-incompatible changes . Some packages are using versioned URLs but most of them aren’t. There is currently no standard tool to handle vendoring . My personal take is to vendor all dependencies with Glide .
It is a good practice to split an application into different packages while the main one stay fairly small. In the hellogopher example, the CLI is handled in the cmd package while the application logic for printing greetings 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 holeLet’s take a look at the various “features” of the Makefile .
GOPATH handlingSince all dependencies are vendored, 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 import path is hellogopher , not github.com/vincentbernat/hellogopher : this shortens imports and makes them easily distinguishable from imports of dependency packages. However, your application won’t be go get -able. This is a personal choice and can be adjusted with the $(PACKAGE) variable.
We just create a symlink from .gopath/src/hellogopher to our root directory. The GOPATH environment variable is automatically exported to the shell commands of the recipes. Any tool should work fine after changing the current directory to $(BASE) . For example, this snippet builds theexecutable:
.PHONY: all all: | $(BASE) cd $(BASE) && $(GO) build -o bin/$(PACKAGE) main.go Vendoring dependenciesGlide is a bit like Ruby’s Bundler . In glide.yaml , you specify what packages you need and the constraints you want on them. Glide computes a glide.lock file containing the exact versions for each dependencies (including recursive dependencies) and download them in the vendor/ folder. I choose to check into the VCS both glide.yaml and glide.lock files. It’s also possible to only check in the first one or to also check in the vendor/ directory. A work-in-progress is currently ongoing to provide a standard dependency management tool with a similarworkflow.
We define two rules:
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 variable to invoke glide . This enables a user to easily override it (for example, with make GLIDE=$GOPATH/bin/glide ).
Using third-party toolsMost projects need some third-party tools. We can either expect them to be already installed or compile them in our private GOPATH . For example, here is the lintrule:
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 $$retAs for glide , we let the user a chance to override which golint executable to use. By default, it uses a private copy. But a user can use its own copy with make GOLINT=/usr/bin/golint .
In , we have the recipe to build the private copy. We simply issue go get to download and build golint . In , the lint rule executes golint on each package contained in the $(PKGS) variable. We’ll explain this variable in the nextsection.
Working with non-vendored packages onlySome commands need to be provided with a list of packages. Because we use a vendor/ directory, the shortcut ./... is not what we expect as we don’t want to run tests on our dependencies. Therefore, we compose a list of packages we care about :
PKGS = $(or $(PKG), $(shell cd $(BASE) && \ env GOPATH=$(GOPATH) $(GO) list ./... | grep -v "^$(PACKAGE)/vendor/"))If the user has provided the $(PKG) variable, we use it. For example, if they want to lint only the cmd package, they can invoke make lint PKG=hellogopher/cmd which is more intuitive than specifying PKGS .
Otherwise, we just execute go list ./... but we remove anything from the vendor directory.
Here are some rules to runtests:
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 invoke tests in differentways:
make test runs alltests;