Hey,

Most of the time I see myself creating a Golang project: be it a one-file single-purpose thing or a more complex project.

To facilitate that process, I try always to keep the structure the same such that there’s no need to put much thought into the process of bootstrapping a new project - for me, the less friction to get started, the better.

The file structure usually looks like this:

.
├── .editorconfig       # Allows you to define and maintain 
│                       # a consistent editor configuration
│                       # for your project.
│ 
├── Dockerfile          # Define the steps to build a Docker
│                       # image that contains just the software
│                       # I'm building and nothing more.
│                        
│                       # The benefits that I see here is that 
│                       # it makes explicit for anyone what are
│                       # the dependencies. It also provides a
│                       # reproducible way for people to build
│                       # your software without worrying about
│                       # external dependencies, like, the 
│                       # compiler toolchain or some shared 
│                       # libraries.
│                       
├── Makefile    
├── lib                 # A package that we might use in `main`
│   ├── foo.go
│   ├── foo_test.go
│   ├── something.go
│   └── something_test.go
├── main.go             # `main` package.
└── VERSION     

Some things to pay attention:

I use vim with set shiftwidth=2; set tabstop=2 set by default, meaning that I always have two spaces set for identation.

Go, on the other hand, is all about tabs.

The standard formatting tool doesn’t even allow you to use spaces - see cmd/gofmt: remove -tabs and -tabwidth flags.

And I get it!

Because with a standard Docker builder we’re able to do the equivalent of vendoring the whole filesystem, it allows the developer to set the versions for all the toolchain around building the code, and that’s very convenient for someone who’s wanting to consume the software from your repository.

Now, let’s get to the point - the Golang Makefile.

The (annotated) minimal makefile for Go

In this case, I extracted the Makefile from a test project I started some time ago (a “load-balancer” that makes use of fasthttp: cirocosta/l7).

If you’ve never used make before (or if you don’t remember the syntax), there’s this awesome Gist from Isaacs.

ps.: the snippet below is scrollable - if you’re seeing the text cut, perform a horizontal scroll.

# I usually keep a `VERSION` file in the root so that anyone
# can clearly check what's the VERSION of `master` or any
# branch at any time by checking the `VERSION` in that git
# revision.
#
# Another benefit is that we can pass this file to our Docker 
# build context and have the version set in the binary that ends
# up inside the Docker image too.
VERSION         :=      $(shell cat ./VERSION)
IMAGE_NAME      :=      cirocosta/l7


# As a call to `make` without any arguments leads to the execution
# of the first target found I really prefer to make sure that this
# first one is a non-destructive one that does the most simple 
# desired installation. 
#
# It's very common to people set it as `all` but it could be anything 
# like `a`.
all: install


# Install just performs a normal `go install` which builds the source
# files from the package at `./` (I like to keep a `main.go` in the root
# that imports other subpackages). 
#
# As I always commit `vendor` to `git`, a `go install` will typically 
# always work - except if there's an OS limitation in the build flags 
# (e.g, a linux-only project).
install:
	go install -v


# Keeping `./main.go` with just a `cli` and `./lib/*.go` with actual 
# logic, `tests` usually reside under `./lib` (or some other subdirectories).
#
# By using the `./...` notation, all the non-vendor packages are going
# to be tested if they have test files.
test:
	go test ./... -v


# Just like `test`, formatting what matters. As `main.go` is in the root,
# `go fmt` the root package. Then just `cd` to what matters to you (`vendor`
# doesn't matter).
#
# By using the `./...` notation, all the non-vendor packages are going
# to be formatted (including test files).
fmt:
        go fmt ./... -v


# This target is only useful if you plan to also create a Docker image at
# the end. 
#
# I really like publishing a Docker image together with the GitHub release
# because Docker makes it very simple to someone run your binary without
# having to worry about the retrieval of the binary and execution of it
# - docker already provides the necessary boundaries.
image:
	docker build -t cirocosta/l7 .


# This is pretty much an optional thing that I tend always to include.
#
# Goreleaser is a tool that allows anyone to integrate a binary releasing
# process to their pipelines. 
#
# Here in this target With just a simple `make release` you can have a 
# `tag` created in GitHub with multiple builds if you wish. 
#
# See more at `gorelease` github repo.
release:
	git tag -a $(VERSION) -m "Release" || true
	git push origin $(VERSION)
	goreleaser --rm-dist

.PHONY: install test fmt release

Save that content in the Makefile file in root directory of the project, create a VERSION file with something like 0.0.1 (semver) and you’re ready to go.

Naturally, vendor directories and all of that is already covered.

Projects with many packages and vendor dependencies

In bigger projects, you might have a bunch of packages and they’re not as flat as I described here.

Regardless of how big it is, given that in our targets we’ve been specifying to the tools that the paths should be recursively traversed (./...), we don’t need to keep updating the Makefile.

Although this one I supplied is minimal, it stays minimal even if your project grows.

It might happen though, that you want to include an extra tool that requires exact paths to directories where Golang files live. Maybe your static analyser doesn’t have the equivalent of ./....

In that case, we can still keep our Makefile minimal.

One way is adapting the Makefile to handle both nesting and multiple packages “intelligently” by finding them ahead of time and then suppliying these to your targets.

First things first, we can combine find and xargs utilities to gather the directories:

GO_SRC_DIRS := $(shell \
	find . -name "*.go" -not -path "./vendor/*" | \
	xargs -I {} dirname {}  | \
	uniq)

GO_TEST_DIRS := $(shell \
	find . -name "*_test.go" -not -path "./vendor/*" | \
	xargs -I {} dirname {}  | \
	uniq)

# Shows the variables we just set.
# By prepending `@` we prevent Make
# from printing the command before the
# stdout of the execution.
show:
	@echo "SRC  = $(GO_SRC_DIRS)"
	@echo "TEST = $(GO_TEST_DIRS)"

Now, testing it:

make show

SRC  = ./lib ./lib/sub ./lib .
TEST = ./lib

With those lists as variables, we can make use of them as dependencies for our new fmt and test targets:

test: $(GO_TEST_DIRS)
	@for dir in $^; do \
		pushd ./$$dir > /dev/null ; \
		go test -v ; \
		popd > /dev/null ; \
	done;

fmt: $(GO_SRC_DIRS)
	@for dir in $^; do \
		pushd ./$$dir > /dev/null ; \
		go fmt ; \
		popd > /dev/null ; \
	done;

As $^ targets all the dependencies, on both targets we’re able to use the selector to iterate over the directories of each.

Note that I’m not using cd to get into the directories. That’s because not knowing how deep in the file structure we’ll go, just stacking the directory changes with pushd is easier as to get back to the original place we just need to popd.

Closing thoughts

I think it’s very handy to keep a standard way of performing basic operations across multiple repositories. In my experience this reduces the friction of moving from one project to another. Having a common flow of how to build, create an image and publish a project using a Makefile has helped me in such area.

Do you think the same? What are your thoughts?

Reach me on Twitter at any time @cirowrc and subscribe to the list if you’re a Golang developer or simply likes software stuff!

finis