TL;DR: Just import and use versioninfo.Revision to automatically include a Git hash in your Go application.

Curious how it works? Want to make your own version info package? Read on.

In previous blog posts, I’ve written about how you can include revision information like a Git revision hash in a Go binary by using linker flags and //go:generate or //go:embed. Those approaches worked, but they required extra manual steps outside of the normal go build or go install commands.

Back in 2020, Brad Fitzpatrick opened an issue to automatically include version control information in Go binaries. He wrote,

Currently many projects do this by hand with a and stamping it manually with --ldflags=-X foo=bar, but that means programs built the normal Go way lack that information, and people end up with non-portable (shell, often) build scripts.

I’ve hit this enough times with my own projects that it’s actively frustrating me. It’s worse when programs are clients that want to report their version number to a server (which might want to do analytics, build horizon enforcement, protocol version negotiation, etc) and then can’t. There are alternative ways to do all that, but they’re tedious.

Mostly I’m concerned that people have bespoke, often non-portable build scripts.

The proposal was approved, and the changes were released along with Go 1.18 in 2022.

The way it works is simple, but it requires a little bit of boilerplate to extract. The runtime/debug package got a ReadBuildInfo() function back in Go 1.12 to support the addition of Go modules. The BuildInfo struct it returns has information about all of the Go modules included in a build. This means that you could look up the version numbers of all of the modules included in a build… except for the main module. 😓 It makes logical sense when you think about it, but it was sort of unfortunate, because the main module is often the module you most want to get the build version for. When you include a Go module in your binary, the Go build tool looks it up online according to its version tag, and then includes exactly that version in your code. But for the main module, there might not be any version tag at all, or even any version control system for that matter, because you can create and run a build whenever and wherever, completely outside of the system for publishing a Go module.

Fortunately, with the changes brought in with Go 1.18, now there is a special field just for the main module that will include version control information if it’s available. BuildInfo.Settings contains a slice of key-value struct string pairs. You can get the information out of it like this:

for _, kv := range info.Settings {
	switch kv.Key {
	case "vcs.revision":
		Revision = kv.Value
	case "vcs.time":
		LastCommit, _ = time.Parse(time.RFC3339, kv.Value)
	case "vcs.modified":
		DirtyBuild = kv.Value == "true"

There are a few more nuances and caveats to be aware of.

One, you can always opt out of including version control information by passing -buildvcs=false to go build. By default, tests don’t include version control information, because it can take some time to include it, and you usually don’t want it.

Two, the build information will just be a Git revision hash (or equivalent for other version control systems), not a Git tag, because you could have multiple tags for the current commit, but there is only one hash for the current commit, so the hash is less ambiguous.

Three, on the other hand, when you install a remote Go binary using go install, it won’t have any version control information, since it was downloaded and built from Go modules, and not from a version control system. In those cases, you can instead read the BuildInfo.Main.Version which will have the Git tag that was used to publish as a Go module.

Again, the TL;DR is to use a package which I published that parses the Git revision info for you, or just take a look at it and write your own version info package. Some nice features of my version info package are that it includes a Short() function which uses the BuildInfo.Main.Version when that’s set, and it also has a helper function to add a -version flag to the standard library command line flags parser. But the code is quite simple and you can write your own version in an afternoon if you want. The main thing is to avoid writing complicated build scripts now that we can all easily include revision information automatically with the normal go build and go install commands.

(Thanks to Francesc Campoy for the share image for this post.)