One of the things that I find really neat about Go, is the single-executable delivery model. For small projects, it’s as simple as dragging and dropping the built executable right where I need it. After some time, however, I might easily forget which version of my codebase a given executable has been built upon. If I have to revert a change, how can I make sure that I build from the right commit?

In large projects, this is where CI/CD comes to save the day. For small prototypes, however, setting up CI might be an unnecessary overkill until a much later stage in the process.

Luckily, there is an easy way to “bake” the information about the current commit hash right into the built executable.

Go does not have a dedicated build system, but the standard practice across the community is to use make. Make is widely available across *nix operating systems (incl. macOS), so I would usually add a simple Makefile to most of my Go projects. In this case, my makefile would look like this:

GOOS=linux
GOARCH=386

.PHONY: build

GIT_COMMIT := $(shell git rev-list -1 HEAD)

build:
	GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "-X main.gitCommit=$(GIT_COMMIT)" .

Two things to note here:

  1. GIT_COMMIT will store of the value of the current commit hash that our code base is at.
  2. -ldflags "-X main.gitCommit=$(GIT_COMMIT)" is Go’s way of telling the linker to pass the commit hash to a variable in the code (main.gitCommit). Think of this as “baking” the information right into the compiled executable.

All we need to make this happen, is use make build instead of go build ...

In our code, we can check for the presence of a flag (e.g. -v) and display the hash:

package main

var gitCommit string

func printVersion() {
	log.Printf("Current build version: %s", gitCommit)
}

func main() {
	versionFlag := flag.Bool("v", false, "Print the current version and exit")
	flag.Parse()

	switch {
	case *versionFlag:
		printVersion()
		return
	}
	// continue with other stuff
}

Related reading:

Make (software) - Wikipedia
In software development, Make is a build automation tool that automatically builds executable programs and libraries from source code by reading files called Makefiles which specify how to derive the target program. Though integrated development environments and language-specific compiler features can also be used to manage a build process, Make remains widely used, especially in Unix and Unix-like operating systems.
Inject build-time variables with Golang
In this blog post I'll show you how to inject variables into your Golang executable at build-time such as a Git Commit digest, then automate it with Docker
GoReleaser
Deliver Go binaries as fast and easily as possible