What build metadata does a Go binary carry, and how do you make builds reproducible?
Question 430MediumGo 1.22 to 1.25
Since Go 1.18 every binary embeds its module path, dependency versions and hashes, build settings (GOOS, GOARCH, CGO_ENABLED, -tags, -ldflags), and VCS information (-buildvcs=auto). You can read it at runtime, or from outside with go version -m ./app, which is handy for auditing vulnerable dependencies in deployed binaries.
package main
import (
"fmt"
"runtime/debug"
)
func main() {
bi, ok := debug.ReadBuildInfo()
if !ok {
return
}
fmt.Println(bi.GoVersion, bi.Main.Path, bi.Main.Version)
for _, s := range bi.Settings {
if s.Key == "vcs.revision" || s.Key == "vcs.modified" {
fmt.Println(s.Key, s.Value)
}
}
}
Reproducibility: Go builds are bit-for-bit reproducible (the toolchain itself has been since 1.21) if you:
- use
-trimpathto remove local filesystem paths, - pin the toolchain (go and toolchain lines),
- use
CGO_ENABLED=0or a pinned C toolchain, - keep timestamps out of
-Xvalues (use the commit time instead).
Gotcha: -buildvcs fails in CI when .git is missing or owned by another user ("error obtaining VCS status"). Use -buildvcs=false or fix safe.directory. Main.Version is (devel) for local builds. Since Go 1.24 it is derived from the VCS tag or pseudo-version.
More on Modules, Packages & Tooling
- Q428How do you cross-compile Go and produce a fully static binary for a scratch container?
- Q429How do you inject a version string at build time with -ldflags? What does this program print?
- Q431What does go vet catch? What does it report for the code below?
- Q432How do staticcheck and golangci-lint differ from go vet? How would you set up linting for a team?
- Q433How does //go:embed work? Show the three target types.
- Q434What are the gotchas of go:embed patterns? Why is my .env or _redirects file missing?