skip to content

What does runtime/debug.ReadBuildInfo() tell a running Go binary about itself?

level: juniorimportance: should knowfreq 42%

answer

  1. the binary can describe itself
  2. one call in runtime/debug
  3. a struct and a boolean
  4. module, deps, toolchain, settings
  5. written at link time, not read from disk

basics

~20 s

ReadBuildInfo returns the build record the linker embedded in the binary: the Go toolchain version, the main module's path and version, the dependency modules linked in, and build settings such as flags and the VCS revision. It reports false when no record is present.

solid answer

~40 s

`debug.ReadBuildInfo()` returns `(*debug.BuildInfo, bool)`. The struct carries `GoVersion` (the toolchain that built it), `Path` (the import path of the main package), `Main` (the main module's path and version), `Deps` (the modules whose packages were actually linked in, each with a version and a `go.sum` hash), and `Settings` — a slice of key/value pairs holding build flags like `-trimpath` and `-tags`, environment such as `GOOS`, `GOARCH` and `CGO_ENABLED`, and the version-control keys `vcs`, `vcs.revision`, `vcs.time` and `vcs.modified`. Nothing is read from disk at run time; it is a blob the `go` command wrote into the binary. The boolean is false when the binary carries no such record, so a `version` subcommand must handle that rather than dereference the pointer.

code

go · 15 lines
go
func printVersion() {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		fmt.Println("no build information embedded")
		return
	}
	fmt.Println("module:", info.Main.Path, info.Main.Version)
	fmt.Println("package:", info.Path)
	fmt.Println("toolchain:", info.GoVersion)
	for _, s := range info.Settings {
		if strings.HasPrefix(s.Key, "vcs.") {
			fmt.Println(s.Key, "=", s.Value)
		}
	}
}

go deeper

for a junior

Be ready to name the call, the two return values, and three or four things the record holds: toolchain version, main module, linked dependency modules, build settings. Say out loud that the second return can be false.

for a middle

Explain that the record is written by the linker at build time rather than read from disk, and that Settings is a key/value slice you walk. Know which keys carry version-control data.

for a senior

Show how you would use it in a shipped artifact: a version output that never panics when the record is absent, and that prints enough for someone holding only the binary to identify the source it came from.

for a principal

Own the question of what a binary must be able to say about itself before it is allowed out, and where that duty sits between the toolchain and the release pipeline.

## What the record is When the `go` command links a binary it writes a small, structured description of the build into the executable itself. `runtime/debug.ReadBuildInfo` hands the running program that description back. It is not a file read, a network call, or a parse of `go.mod` at start-up — `go.mod` need not exist on the machine running the binary. It is data frozen at link time, which is exactly why it can be trusted to describe *this* executable rather than whatever source happens to be lying around. ```go info, ok := debug.ReadBuildInfo() ``` The signature is `func ReadBuildInfo() (info *BuildInfo, ok bool)`. `ok` is false when the binary has no embedded record — for example one not produced by the `go` command in module mode, or one a post-processing tool has mangled. Treat it as a real branch: printing `unknown` beats a nil dereference inside a `version` subcommand. ## The fields `debug.BuildInfo` has five fields: - **`GoVersion`** — the toolchain string the `go` command recorded, e.g. `go1.27.0`. - **`Path`** — the import path of the main package that was built. - **`Main`** — a `debug.Module` for the main module: `Path`, `Version`, `Sum`, and a `Replace` pointer. `Version` holds a real version only when the toolchain could determine one; for many builds straight out of a working tree it is the placeholder `(devel)`, which is why the commit hash, not this field, is usually what identifies the build. - **`Deps`** — `[]*debug.Module` for the module dependencies whose packages were **linked into this binary**. That is narrower than the module graph and narrower than the `require` list in `go.mod`: a module you require but never import contributes nothing here. Each entry carries the selected version, the `go.sum` hash in `Sum`, and, if a `replace` directive redirected it, the substitute in `Replace`. - **`Settings`** — `[]debug.BuildSetting`, each a `Key`/`Value` pair. Keys include build flags (`-tags`, `-trimpath`, `-ldflags`, `-buildmode`, `-race`), toolchain environment (`GOOS`, `GOARCH`, `CGO_ENABLED`, `DefaultGODEBUG`), and the version-control keys `vcs`, `vcs.revision`, `vcs.time` and `vcs.modified` when the build was stamped from a repository. `Settings` is a slice, not a map, so reading one value means walking it and matching on `Key`. ## Printing it `(*BuildInfo).String()` renders the whole record as text, and `debug.ParseBuildInfo` reads that text back — handy if you want to store the record next to a released artifact. The on-disk counterpart is `go version -m <binary>`, which prints the same embedded record for a file you are holding rather than a process you are running. A `version` subcommand that prints the record and a release process that keeps the `go version -m` output of what it published are answering the same question from the two ends. ## What it is not `runtime.Version()` also returns a Go toolchain version string, but it comes from the runtime linked into the binary rather than from the build record, so it still works when `ReadBuildInfo` reports false. For an ordinary build the two agree. The record contains no wall-clock build time, no CI job identifier, no release channel, and no environment name. Go leaves the clock out deliberately — embedding it would make two builds of identical source differ byte for byte. Anything about the *build event* rather than the *source* has to be injected at link time by whoever runs the build; the toolchain will not invent it. ## Why anyone cares Months after a release, the useful question is not "what does the deploy dashboard say" but "what is this binary". A binary that can describe itself answers that without a build-system archaeology dig, and the answer travels with the artifact wherever it is copied.

  • When does ReadBuildInfo return false?
    When the executable carries no embedded build record: it was not produced by the `go` command in module mode, it came from a different toolchain or linker, or a post-processing step damaged the record. Handle it as a normal branch and fall back to `runtime.Version()`, which reads the toolchain version out of the runtime rather than the record.
  • How does BuildInfo.Deps differ from the require list in go.mod?
    `Deps` lists only the modules whose packages were actually linked into this binary, with the version that module resolution selected, its `go.sum` hash, and any `replace` target. A module required in `go.mod` but never imported does not appear. It describes the build, not the module graph.
  • How do you get the same record for a binary you are not running?
    `go version -m ./app` prints the embedded record straight from the file — toolchain version, main module, linked modules and the build settings. `(*BuildInfo).String()` produces that same text from inside the process, and `debug.ParseBuildInfo` reads it back.

saying these in an interview costs you the question

  • Thinks ReadBuildInfo parses go.mod at run time
  • Ignores the ok result and dereferences a nil pointer
  • Expects a wall-clock build timestamp in the record
  • Confuses Deps with the whole module graph
  • Assumes Main.Version always holds a semver release