Setting variables at build-time can provide valuable metadata to our application that wasn’t available when writing the code or even at runtime. We can control feature-flags, or build information, like a version number, without updating the Go-code constantly.

Versioning a Go Project

Imagine we have version/version.go in the main package github.com/benweidig/goapp:

package version

import (
	"fmt"
	"time"
)

var (
	Version = "dev"
	CommitHash = "n/a"
    BuildTimestamp = "n/a"
)

func BuildVersion() string {
	return fmt.Sprintf("%s-%s (%s)", Version, CommitHash, BuildTimestamp)
}

The three properties are identifying any build:

  • Version: the version identifier of your application.
  • CommitHash: for reproducible builds, it’s important to know which commit was used to create a specific binary.
  • BuildTimestamp: a dynamic build-context value that provides info about the build itself. We could also use the build-number of the CI/CD-pipeline instead instead.

Of course, we could just update the Version every time before building a new binary. But the CommitHash is a value that can’t be set before committing the code itself: the hash of the commit used to build the application wouldn’t be the one set in the code. BuildTimestamp depends on the build itself and can’t be set beforehand.

We will set these values at build-time instead of updating our code before compiling it with the help of linker flags. They help us to make our builds reproducible and automate the process of creating a meaningful version identifier. But be aware that the dynamic BuildTimestamp will make the result of the compilation non-deterministic. Even thought the correct source code is identified by the CommitHash, the resulting binary won’t be the same due to the different BuildTimestamp.

All these properties are needed to create reproducible builds. So we will set them at build-time instead of updating our code before compiling it with the help of linker flags.

Linker Flags to the Rescue

The Go build process – go build – supports linker flags, that will be passed to the underlying go tool link call, allowing us to set public variables in your code at build-time.

To pass a linker flag, we have to add the -ldflags argument:

go build -ldflags="-linkerflag"

The linker flag we’re interested in is -X, which sets a variable at link-time:

go build -ldflags="-X 'full_package_path.variable=value'"

This big caveat is that only string values are allowed, as you might have already guessed from how the values are quoted.

In case of our version.go file the call would look like this:

go build -ldflags="-X 'github.com/benweidig/goapp/version.Version=1.0.0'"

Instead of writing the version directly in the call, we should use a script to derive the required values automatically.

Build Scripts

So far we’ve just moved the burden of writing the current version from the Go file to the build command. Let’s automate that step with a script git. The other properties can also be derived in such a script:

#!/usr/bin/env bash

# STEP 1: Determinate the required values

PACKAGE="github.com/benweidig/goapp"
VERSION="$(git describe --tags --always --abbrev=0 --match='v[0-9]*.[0-9]*.[0-9]*' 2> /dev/null | sed 's/^.//')"
COMMIT_HASH="$(git rev-parse --short HEAD)"
BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')

# STEP 2: Build the ldflags

LDFLAGS=(
  "-X '${PACKAGE}/version.Version=${VERSION}'"
  "-X '${PACKAGE}/version.CommitHash=${COMMIT_HASH}'"
  "-X '${PACKAGE}/version.BuildTime=${BUILD_TIMESTAMP}'"
)

# STEP 3: Actual Go build process

go build -ldflags="${LDFLAGS[*]}"

The general concept of the script should be easily adaptable to your own CI/CD needs. Personally, I use a Makefile for my Git-helper Tortuga.

Conclusion

Injecting dynamic values at build-time with linker flags is a powerful addition to your toolbelt. You can provide build information as shown in the article or use it to control feature flags, or anything else that depends on the build context itself, and not the information available at runtime.