2025-04-20 Build Info In Go Binaries

I like having assorted build info in my Go binaries and here’s how I do it.

Attention

Shortly after writing this, I learned that as of Go 1.18 (2022), Go embeds source version information like commit hash and build time in binaries by default and you can get it as a BuildSetting with runtime/debug, so I no longer need to DIY it with -ldflags at build time, yay!

I first started doing this in 2020 with Go 1.13, so it was a useful approach for two years or so - but this is a great example of the value of reading release notes…!

I’m leaving this here because I think the general technique is potentially helpful, and translateable to languages that don’t just handle it for you.

At the core: Go linker flags, specifically -X importpath.name=value - “Set the value of the string variable in importpath named name to value.”

Other methods exist but this is the right combination of powerful, resilient, and clear for me.

This lets me do this in my code:

var GIT_VERSION string
var GIT_BRANCH string
var BUILT_AT string

And then this at build time (taken from my Makefile, hence the $$):

-ldflags "-X 'main.GIT_VERSION=$$(./git-version)' -X 'main.BUILT_AT=$$(date)' -X 'main.GIT_BRANCH=$$(git symbolic-ref --short HEAD)'"`

That gives me:

  • BUILT_AT which is just the build timestamp - useful to make sure I’ve not missed restarting a service to pick up changes etc.

  • GIT_BRANCH which is, unsurprisingly, the name of the branch - useful to see if I’m running on a main build, or something from a feature branch

  • GIT_VERSION which is the output of a short script that gives me a slightly more detailed idea of what I built from

git-version is:

#!/bin/bash

HEAD_HASH=$(git rev-list -1 HEAD)

GITVERSION=$HEAD_HASH

# https://stackoverflow.com/a/2659808
DIRTY=$(git diff-index --quiet HEAD -- || echo '-DIRTY')

GITVERSION+=$DIRTY

echo "$GITVERSION"

which outputs the commit hash as a string, plus a -DIRTY suffix if there are uncommitted changes - useful to know exactly what I built from, and/or if there was anything funky - e.g.:

$ ./git-version
ace0fba5eace0fba5eace0fba5eace0fba5e-DIRTY

Then I can simply use those as needed, e.g. for dumping information when running with --version:

$ ./mybin --version
GIT_VERSION: ace0fba5eace0fba5eace0fba5eace0fba5e-DIRTY
GIT_BRANCH: feature/foo
BUILT_AT: Mon 30 Feb 1712 03:13:37 CEST

I don’t write a huge amount of Go, so it wouldn’t surprise me to learn that there are better ways / room for improvement, but this works nicely for me!

Update

I shared this while noting that I was conscious that my use of BUILT_AT screwed my build determinism but I hadn’t gotten round to figuring out a better way.

Chris McDonald helpfully pointed me at the SOURCE_DATE_EPOCH environment variable which is exactly for this kind of thing!