In February I started to show how awesome Go 1.18 is. That time, it was still in beta, but now it’s officially released and 1.18 is the latest stable version. I did not have time to publish more on the topic, but I’ll try my best to keep it up.
I read an awesome article from Christian Rebischke about the changes
in the runtime/debug package, but I though we can do more if we are clever.
So the article shows, we can access VCS information with BuildInfo. Sadly it
knows only the revision, date, and the “dirty” state. That’s good, and I know a
few applications where they use only revision or date for versioning, like
mc (MinIO command line tool) has a very simple version number:
❯ mc --version
mc version RELEASE.2022-03-31T04-55-30Z
It’s sad it does not contain information about git tags, but after reading that article I though there should be a way we can get tags into our application without using global variables. If there is a way to achieve this, I’ll find it, and for sure I found a way.
BuildInfo
First of all, BuildInfo.Settings is a key-value list.
type BuildInfo struct {
GoVersion string // Version of Go that produced this binary.
Path string // The main package path
Main Module // The module containing the main package
Deps []*Module // Module dependencies
Settings []BuildSetting // Other information about the build.
}
type BuildSetting struct {
// Key and Value describe the build setting.
// Key must not contain an equals sign, space, tab, or newline.
// Value must not contain newlines ('\n').
Key, Value string
}
With a simple application we can check what’s inside the BuildInfo struct by
default if we are inside a git repository.
package main
import (
"fmt"
"runtime/debug"
)
func main() {
info, _ := debug.ReadBuildInfo()
fmt.Println(info)
}
And the output is:
❯ go run .
go go1.18
path build-info-check
mod build-info-check (devel)
build -compiler=gc
build CGO_ENABLED=1
build CGO_CFLAGS=
build CGO_CPPFLAGS=
build CGO_CXXFLAGS=
build CGO_LDFLAGS=
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1
build vcs=git
build vcs.revision=6aff455dea150ab05d71da4aba69de2af3a10c38
build vcs.time=2022-04-04T11:05:34Z
build vcs.modified=true
Let’s try to add an ldflag as we did before to set versions with global
variables.
❯ go run -ldflags="-X main.Version=0.0.1" .
go go1.18
path build-info-check
mod build-info-check (devel)
build -compiler=gc
build -ldflags="-X main.Version=0.0.1"
build CGO_ENABLED=1
build CGO_CFLAGS=
build CGO_CPPFLAGS=
build CGO_CXXFLAGS=
build CGO_LDFLAGS=
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1
build vcs=git
build vcs.revision=6aff455dea150ab05d71da4aba69de2af3a10c38
build vcs.time=2022-04-04T11:05:34Z
build vcs.modified=true
We have that information in the binary. We can say “that’s it, done”, but not
yet. That would be ugly and potentially hurtful to set a global variable. What
if (should not) the application has a Version variable for a totally
different purpose?
ldflags
We can do a lot of things with ldflags. There is an interesting linker flag:
-buildid id
record id as Go toolchain build id
Disclaimer: I don’t know if it’s used by Go by default. I did not make
extensive testing, it is possible it can break things. Right now, I don’t see
how and where, but keep that in mind. The basic logic is the same from here.
If it breaks things, we can go back to -X main.DefinitelyNotDefinedVersion.
❯ go run -ldflags="-buildid=v0.0.1" .
go go1.18
path build-info-check
mod build-info-check (devel)
build -compiler=gc
build -ldflags=-buildid=v0.0.1
build CGO_ENABLED=1
build CGO_CFLAGS=
build CGO_CPPFLAGS=
build CGO_CXXFLAGS=
build CGO_LDFLAGS=
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1
build vcs=git
build vcs.revision=6aff455dea150ab05d71da4aba69de2af3a10c38
build vcs.time=2022-04-04T11:05:34Z
build vcs.modified=true
Yes, it’s there, we can parse it runtime. We have to iterate through the build
settings list and find a -ldflags key and a value that starts with
-buildid=. While we are there, we can save all other information as well, they
can be useful.
type versionInformation struct {
Version string
Time string
Revision string
Dirty bool
}
func findVersion() versionInformation {
version := versionInformation{}
info, _ := debug.ReadBuildInfo()
fmt.Println(info)
for _, setting := range info.Settings {
if setting.Key == "-ldflags" && strings.HasPrefix(setting.Value, "-buildid=") {
version.Version = strings.TrimPrefix(setting.Value, "-buildid=")
} else if setting.Key == "vcs.revision" {
version.Revision = setting.Value
} else if setting.Key == "vcs.time" {
version.Time = setting.Value
} else if setting.Key == "vcs.modified" {
version.Dirty = true
}
}
return version
}
func main() {
fmt.Println(findVersion())
}
With this, we have a nice compact version information in an easy to use struct.
❯ go run -ldflags="-buildid=v0.0.1" .
{v0.0.1 2022-04-04T11:05:34Z 6aff455dea150ab05d71da4aba69de2af3a10c38 true}
We can even implement a String() string function on versionInformation:
func (info versionInformation) String() string {
parts := []string{AppName}
if info.Version != "" {
parts = append(parts, info.Version)
} else if info.Revision != "" {
parts = append(parts, info.Revision)
}
if info.Dirty {
parts = append(parts, "*dirty*")
}
if info.Time != "" {
parts = append(parts, fmt.Sprintf("(%s)", info.Time))
}
return strings.Join(parts, " ")
}
And now, we have a well formatted version information as a string:
❯ go run -ldflags="-buildid=v0.0.1" .
MyFancyApp v0.0.1 *dirty* (2022-04-04T11:05:34Z)
But that’s the same thing with extra steps
Indeed, it’s much longer, but does not use global variables and I hate global variables.
And I already published a module just for that. With that package, the final output will be much cleaner:
package main
import (
"fmt"
"github.com/go-asset/build"
)
const AppName = "MyFancyApp"
func main() {
version, _ := build.ReadVersion(AppName)
fmt.Println(version)
}
With this package, we get the same output:
❯ go run -ldflags="-buildid=v0.0.1" .
MyFancyApp v0.0.1 *dirty* (2022-04-04T11:05:34Z)
There is a catch, while built-in VCS information is not removed with -s
(disable symbol table), ldflags will not survive that.