← Home

The Most Exciting Feature of Go 1.18

It’s been 5 years since I wrote about the most exciting feature of Go 1.8 and there have been significant improvements in the language, toolchain and ecosystem since then. I’m not going to cover any of that… Go 1.18 is around the corner, being set to release in February of 2022, and the release notes already describe a very exciting, albeit small, feature.

Go 1.18 brings generics, fuzz testing, improved support for IP address types and faster image drawing operations amongst other things. This is an impressive release, with the hype train definitely focusing on generics. However, the most exciting feature, for me at least, is that the version control information is going to automatically be embedded in compiled binaries.

What is this useful for?

Injecting build parameters into your application provides critical data for observability. I have been adding commit hashes into logger contexts for years now. Google Cloud will read the serviceContext object in any log message and make use of the data in error reporting. This can provide great insight into when errors were introduced and what the error rates are for different versions of your service. I’m sure other cloud providers offer similar functionality as well.

How have we done this in the past?

There have been a variety of methods for doing this before Go 1.18, there is the very handy govvv library, you could use the more recent embed directive. Don’t forget the swiss army knife that is -ldflags, which is powerful but has brittle and complex syntax. These are perfectly fine solutions, but they all require an extra step, dependency or something else to learn. As of Go 1.18 we can do this with pure Go code and the standard toolchain.

How do we do this now?

You’ll need to install gotip because at the time of this post Go 1.18 is not yet released.

$ go install golang.org/dl/gotip@latest
$ gotip download

You can now use gotip in place of the go command.

$ gotip version
go version devel go1.18-b74f2efc47 Sat Nov 6 00:29:44 2021 +0000 darwin/amd64

If you’re using Visual Studio Code and the vscode-go extension, then there is a command provided to easily choose the Go environment that you want to use. Simply search for “go environment” in the command palette, or click on the Go version in the bottom left of the editor.

Let’s write a sample application to inspect the new build information.

package main

import (
	"fmt"
	"runtime/debug"
)

func main() {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		panic("could not read build info")
	}

	for _, setting := range info.Settings {
		fmt.Printf("%s: %s\n", setting.Key, setting.Value)
	}
}

Running the above code will print the following output:

compiler: gc
tags: goexperiment.regabiwrappers,goexperiment.regabireflect,goexperiment.regabiargs,goexperiment.pacerredesign
CGO_ENABLED: true
CGO_CPPFLAGS:
CGO_CFLAGS:
CGO_CXXFLAGS:
CGO_LDFLAGS:
gitrevision: 0b1cb25494ccc504337fb5fe916291fb7d680823
gitcommittime: 2021-10-27T07:20:41Z
gituncommitted: true

If you don’t see the git keys, make sure that you are building or running the entire package and not a single file. I’m so used to go run main.go that it took me a while to figure this part out. If you read the documentation you’ll see mention of the main package and main module… this implies the requirement to build a package for build information to be included.

Build packages, not files

“Version control information is embedded if the go command is invoked in a directory within a Git, Mercurial, Fossil, or Bazaar repository, and the main package and its containing main module are in the same repository. This information may be omitted using the flag -buildvcs=false.” — https://tip.golang.org/doc/go1.18

# These will work
gotip run .
gotip run ./...
# This will not
gotip run main.go

Using build info with the zap logger

Here’s another example of how one would include this information in a logger context.

package main

import (
	"runtime/debug"

	"go.uber.org/zap"
)

func main() {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		panic("could not read build info")
	}

	logger, _ := zap.NewProduction()
	defer logger.Sync()

	for _, setting := range info.Settings {
		if setting.Key == "gitrevision" {
			logger = logger.With(zap.String("version", setting.Value))
		}
	}

	logger.Info("Hello, World!")
}

Running the above application will print the following:

{
  "level": "info",
  "ts": 1636189230.721092,
  "caller": "buildinfo/main.go:24",
  "msg": "Hello, World!",
  "version": "0b1cb25494ccc504337fb5fe916291fb7d680823"
}

Reproducible builds

“Please consider omitting build time from your artifacts. It makes build reproducibility a nightmare; people are coming up with frameworks to lie to build systems about the current time. https://reproducible-builds.org” — rollcat on hacker news

This new build info is very exciting, but be careful which data you decide to include in your build artifacts regardless of how you are doing so. We should be striving for reproducible builds, and some of these variables, like the date, will be different for each build. These variable factors should be excluded from your binary.

Further reading