Build Flags and Version Stamping
Pass -ldflags to stamp a version into the binary and -trimpath to keep builds reproducible.
go build -trimpath -ldflags "-s -w -X main.version=1.4.0" -o bin/app ./cmd/appA build flag is an option you give to go build. It changes the binary that comes out without changing a line of source. Two flags matter when you ship: -ldflags and -trimpath.
Stamping a version
-ldflags passes arguments to the linker, the tool that assembles the final executable. The useful linker argument is -X importpath.name=value, documented as: set the value of the string variable in importpath named name to value. The documentation is strict about when it works: only if the variable is declared uninitialised, or initialised to a constant string expression. So var version = "dev" can be stamped. A version assembled by a function call at startup cannot. -s omits the symbol table and debug information and -w omits the DWARF symbol table, which makes the file smaller and stack traces less useful. -ldflags "..." and -ldflags="..." are the same thing written two ways.
Reproducible builds
-trimpath removes all file system paths from the resulting executable. Without it the binary carries absolute paths from the machine that built it, including your home directory. With it, recorded file names begin with a module path and version instead, so two people building the same commit get the same bytes.
You may not need -X for the commit
Since Go 1.18 the go command stamps version control details on its own: vcs.revision, vcs.time and vcs.modified. The -buildvcs flag controls it and defaults to auto, which stamps when the main package, its module and the current directory are in the same repository. Read them at runtime with debug.ReadBuildInfo, or from outside with go version -m bin/app. Keep -X for what the repository does not know, such as a release tag.
Example
package main
import "fmt"
// version and commit are replaced at link time by:
//
// go build -ldflags "-X main.version=1.4.0 -X main.commit=9f2c1ab" .
//
// Both must stay plain string variables holding a constant, or -X is ignored
// and these defaults survive into the shipped binary.
var (
version = "dev"
commit = "none"
)
func main() {
fmt.Printf("app %s (commit %s)\n", version, commit)
}
// Built with a plain `go build .`, with no -ldflags at all:
//
// Output:
// app dev (commit none)When to use it
- Make a /version endpoint report the exact release tag and commit of the running binary.
- Shrink a shipped binary by dropping debug information with -s -w.
- Produce byte-identical builds from the same commit so an image digest can be compared.
More examples
Read the revision the toolchain stamped for you
debug.ReadBuildInfo has the signature func ReadBuildInfo() (info *BuildInfo, ok bool), and info.Settings is a slice of key and value pairs recorded at build time. Keys include CGO_ENABLED, GOOS, GOARCH, vcs.revision, vcs.time and vcs.modified. Use this when you want the running service to report its exact commit and you would rather not maintain an -ldflags incantation for it.
package main
import (
"fmt"
"runtime/debug"
)
// buildRevision returns the commit go build recorded, or "unknown".
func buildRevision() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "unknown"
}
for _, s := range info.Settings {
if s.Key == "vcs.revision" {
return s.Value
}
}
return "unknown"
}
func main() {
fmt.Println("revision:", buildRevision())
}A release build, then read back what it holds
Several linker arguments go inside one quoted -ldflags value, separated by spaces. go version -m reads a finished binary, so it also works on a file someone handed you with no source. Reach for this in a release script, and in support, when you need to prove which commit a deployed binary came from.
go build -trimpath \
-ldflags "-s -w -X main.version=1.4.0 -X main.commit=$(git rev-parse --short HEAD)" \
-o bin/app ./cmd/app
# Print the module, dependency versions and build settings inside the binary.
go version -m bin/app
# And what the program itself reports.
./bin/app
# app 1.4.0 (commit 9f2c1ab)Stamping a variable in a package other than main
main.version works because main is the import path of the main package. Anywhere else you must write the full module path plus the package directory, which is the most common reason -X appears to do nothing. Use a small build package when the version is needed by several packages, such as a health endpoint and a log line.
// Package build holds values the linker fills in.
package build
// Version is set with:
//
// -ldflags "-X example.com/app/internal/build.Version=1.4.0"
//
// The name before the dot is the package's full import path, not its short name.
var Version = "dev"
// Short returns the version without a leading "v", for display.
func Short() string {
if len(Version) > 1 && Version[0] == 'v' {
return Version[1:]
}
return Version
}
Discussion