What does go build -ldflags "-X main.version=1.4.0" do to a Go binary?
answer
- one flag, handed to the linker
- the pipeline knows it, the source does not
- the target name carries its package
- strings only, written at link time
basics
~20 sIt tells the Go linker to set the package-level string variable named version in package main to 1.4.0 when the binary is linked, so the compiled program can report that version without it being hard-coded in the source.
solid answer
~40 s`-ldflags` passes its argument through to the Go linker, and `-X importpath.name=value` sets a package-level string variable at link time. So `-X main.version=1.4.0` finds the variable `version` declared in package `main` and gives it the value `1.4.0` in the linked binary, replacing whatever constant initialiser the source had. The point is that one unchanged source tree can produce builds that name themselves: the release pipeline computes the version, the commit sha and the build timestamp and injects them, so nothing has to be edited or committed per release. Two constraints matter. It works on strings only — you cannot inject an int, a bool or a struct field. And the target name is package-qualified: `main.version` for the main package, or the full import path such as `example.com/daemon/internal/build.Version` for anything else.
code
go · 10 linespackage main
import "fmt"
// "dev" is what an un-stamped local build reports.
var version = "dev"
func main() {
fmt.Println("daemon", version)
}go deeper
Be ready to write the flag from memory and say where the value lands: a package-level string variable, chosen when the binary is linked. Remember that variables in the main package are addressed as main.something.
Explain why the linker can only rewrite string variables and why the target must be the full import path rather than the short package name. Describe the pipeline flow that produces the value.
Show how stamping fits the release pipeline end to end, including what an un-stamped local build reports and how logs and support tickets let you tell a pipeline artefact from a laptop build.
Own the convention itself: one agreed variable name in one agreed package across every service, so dashboards, log fields and support tooling read build identity the same way everywhere.
## The problem it solves A long-running daemon should be able to answer the question "which build are you?" — in its start-up log line, behind a `--version` flag, on an HTTP endpoint. The naive way is to write the version into the source and commit it before every release. That means a source change per release, a merge conflict magnet, and a value that is wrong the moment someone builds the same commit twice. Go's answer is to let the *linker* write the value in, long after the source was written. The release pipeline already knows the version, the commit sha and the time; it hands them to the build. ## The mechanism `go build` accepts `-ldflags`, whose argument is passed on to the Go linker. One of the linker's flags is: -X importpath.name=value The linker locates the symbol for the package-level variable `name` inside the package whose import path is `importpath`, and writes `value` into its data slot in the output binary. There is no code generation, no source rewriting and no run-time lookup: the binary simply contains that string where the compiled initialiser used to be. A declaration like `var version = "dev"` compiles to a variable in the program's data, initialised from a constant. The linker can overwrite that constant. That is the whole trick, and it explains every restriction that follows. ## Naming the target The name is the package's **import path**, a dot, and the variable's identifier: - For `package main`, the import path is simply `main`, so the target is `main.version` — regardless of what your module is called or which directory the file lives in. - For any other package, you need the full path: `example.com/daemon/internal/build.Version`, not `build.Version` and not `internal/build.Version`. That trips people up because the short package name is what you write in Go code, while the linker matches on the path. Note also that the variable in a non-main package must be **exported** if any other package is going to read it, and the package has to actually be linked into the binary — a package nothing imports is not in the output, and there is no symbol to set. ## Types it can set Strings, and only strings. There is no `-X` form for an int, a bool, a slice or a field of a struct. In practice this is barely a constraint: version, commit sha and build timestamp are all textual. If you genuinely need a number, inject it as a string and parse it once at start-up. The target must also be a `var`, not a `const`. A Go constant is folded into every place it is used and has no storage of its own, so there is nothing for the linker to rewrite. ## Injecting several values All the `-X` definitions go inside a single `-ldflags` argument, separated by spaces: go build -ldflags "-X main.version=1.4.0 -X main.commit=9f2c1ab -X main.buildTime=2026-08-14T09:00:00Z" -o daemon . Because the whole thing is one shell argument, it has to be quoted. If an individual value contains a space, quote that definition too: `-ldflags "-X 'main.notes=built by CI'"`. ## The un-stamped default is a feature A plain `go build` with no `-ldflags` leaves the variable at its source value. Initialising it to `"dev"` rather than `""` is worth doing: it means a developer's laptop build says `dev` in its logs, and a binary that reports `dev` in production is immediately recognisable as something the release pipeline did not produce. ## What it is not - It is not an environment lookup. The value is fixed when the binary is linked; changing an environment variable afterwards changes nothing. - It is not a source edit. The file on disk is untouched, which is why the same commit can produce differently-stamped binaries. - It is not verified. The linker takes whatever string you pass; if your pipeline computes the wrong version, you get the wrong version, stamped confidently. ## Where it sits in a build Stamping flags share the `-ldflags` string with any other linker flags a release build uses, so a release command commonly carries both the `-X` definitions and the size-reducing flags at once. It is the same link step; the flags are just concatenated.
- Can -X set an integer build number or a boolean flag?No. The linker only rewrites package-level variables of type string, so an int, a bool, a struct field or a map entry cannot be injected. If you need a number, inject it as a string and parse it once at start-up. In practice the values you want to stamp — version, commit, build time — are textual anyway.
- How do you stamp several values in one go build?Repeat -X inside the same -ldflags argument: `-ldflags "-X main.version=1.4.0 -X main.commit=9f2c1ab"`. The whole argument is one shell word, so it must be quoted, and if an individual value contains a space you quote that definition as well, as in `-ldflags "-X 'main.notes=built by CI'"`.
- What does the variable hold in a plain go build with no -ldflags?Its source initialiser. That is why teams initialise it to something like `"dev"` rather than the empty string: a developer's local build then says `dev` in its logs, and any binary reporting `dev` in production is visibly not a pipeline artefact.
saying these in an interview costs you the question
- Says -X can set any variable type, not only strings
- Writes -X version=1.4.0 with no package qualifier
- Thinks -X edits the source file before compiling
- Believes the value is read from an environment variable at run time
- Tries to stamp a const rather than a var