How To Embed Versioning Information In Go Applications

You’d like to have your Go applications self-identify with versioning information that gets automatically attached/inserted during each build. Here’s how.

What I Don’t Cover: What versioning scheme you should use, or how to iterate your versions.

Requirements

  1. For all these solutions, we have to utilize some sort of make system. I have chosen to use bash scripts, however you could use https://github.com/magefile/mage, or any scripting language.
  2. The versioning information must be exportable, allowing it to be used in code from anywhere.

A Solution: sed

This is a super easy way to get started with versioning information. All we are doing is creating a special versioning file in our project; let’s call it version.go. In this file, we have several pre-defined variables that get replaced by a build script during the compilation process. This file will be used throughout this tutorial, so let’s define it now:

package version
//version.go
var (
BuildVersion string = ""
BuildTime string = ""
)

In our build script build.sh, we would do something like:

#!/usr/bin/env bashTIME=$(date)
VERSION=2
sed -i "s/BuildVersion string = "[^"]*"/BuildVersion string = "${VERSION}"/" version.go
sed -i "s/BuildTime string = "[^"]*"/BuildTime string = "${TIME}"/" version.go
go build .

Which would allow us to just call ./build.sh and away we go!

This is simple enough, but it has one major downside: every time you build you’ll be changing the contents on disk, meaning that you’ll be constantly committing these changes to your repo. This adds extra commits, with the version.go file changing often. This is a no-go.

A Better Solution: ldflags for variables in main.go

A better way would be just to change these at compile time, with no changes to our source code. Fortunately Go has a way to do this using LD flags. There are several ways to go about this, so I’m going to cover several iterations.

The first, which doesn’t meet all of the requirements, but is the easiest to implement, requires putting the versioning variables in your main.go file.

package main
//main.go
var (    
BuildVersion string = ""
BuildTime string = ""
)
func main() {
//do something with BuildTime/BuildVersion
}

Then to update the version during compilation, something like this might fit the bill:

#!/usr/bin/env bash
version=2
time=$(date)
go build -ldflags="-X 'main.BuildTime=$time' -X 'main.BuildVersion=$version'" .

We could then run this just like the prior script for every build: ./build.sh

However, this solution has one major drawback: the variables can’t be exported, and therefore couldn’t be used elsewhere in your application.

A Great Solution: ldflags for variables in a sub-package

To fix the issue where the variables are not exportable, we just move the versioning variables back to our custom version.go file, and change our ldflags appropriately. However, since we are not referring to the main application anymore but rather a dependency, we have to fully namespace the variables.

If, for example, my $GOPATH contained the following project: github.com/polyverse-security/demo-version, and the project contained version/version.go, then our bash script would be changed to:

#!/usr/bin/env bash
version=2
time=$(date)
go build -ldflags="-X 'github.com/polyverse-security/demo-version/version.BuildTime=$time' -X 'github.com/polyverse-security/demo-version/version.BuildSha=$version'" .

Awesome! We’ve now got versioning information embedded into our applications at compile time. They are exportable, available anywhere during the entire lifecycle of the application, and will be atomically attached to the resulting executable.

Further reading: govvv

If you feel comfortable with the above, and don’t need anything too custom, there is a great library that will help with much of this boilerplate. Introducing https://github.com/ahmetb/govvv. Feel free to investigate and determine if it fits your requirements.

Cheers, and happy versioning!

The registered trademark Linux® is used pursuant to a sublicense from the Linux Foundation, the exclusive licensee of Linus Torvalds, owner of the mark on a world­wide basis.