🪟 Windows TippsModify Windows Support Phone Number with PowerShell(03.09.2026 um 00:00 Uhr)
🔧 AI Nachrichten Podcast: ChatGPT schwatzt Nutzern in Deutschland jetzt Werbung auf(28.08.2026 um 08:46 Uhr)
🪟 Windows TippsMicrosoft bringt Emoji 17.0 auf Windows 11(31.08.2026 um 08:16 Uhr)
🪟 Windows TippsModify Windows Support Phone Number with PowerShell(03.09.2026 um 00:00 Uhr)
🔧 AI Nachrichten Podcast: ChatGPT schwatzt Nutzern in Deutschland jetzt Werbung auf(28.08.2026 um 08:46 Uhr)
🪟 Windows TippsMicrosoft bringt Emoji 17.0 auf Windows 11(31.08.2026 um 08:16 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 7 Min Lesezeit
0

Injecting Version Info at Build Time in Go With -ldflags

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht


  • Book: — the companion book in the Thinking in Go series


  • My project: — an IDE for developers who ship with Claude Code and other AI coding tools


  • Me:






A pager goes off at 2am. Latency spiked on one service in a

fleet of forty. You SSH into the box, run the binary with

--version, and it prints dev. Now you have no idea which

commit is actually running, whether the rollback landed, or if

this box even got the last deploy. The one piece of metadata

that would answer the question was never stamped into the

build.



Go gives you two ways to fix that, and they solve slightly

different problems. One is -ldflags -X, where you feed values

in at build time. The other is runtime/debug.BuildInfo, where

the toolchain already recorded your git revision for free since

Go 1.18. Most codebases use one and forget the other exists.





The one-liner everyone copies wrong



You've seen this in a Makefile somewhere:




CODE
package main

import "fmt"

var version = "dev"

func main() {
fmt.Println("version:", version)
}






Then the build:




CODE
go build -ldflags "-X main.version=1.4.2" -o app






Run it and you get version: 1.4.2 instead of dev. The

linker overwrote the variable's initial value at link time.



The part people get wrong is the target name. -X takes

importpath.name. For a variable in package main that's

main.version. For a variable anywhere else, it's the full

import path of the package, not just the package name:




CODE
go build -ldflags \
"-X github.com/you/app/internal/build.Version=1.4.2"






Get the path wrong and nothing happens. No error, no warning.

The linker silently ignores an -X flag whose target it can't

resolve, and you're left staring at dev wondering why.





The rules -X actually enforces



-X is not a general variable setter. It has three

constraints, and each one produces a silent no-op when you

break it.



The target must be a package-level var of type string. Not

a const. Not an int. A const is baked in at compile time

before the linker runs, so there's nothing to overwrite:




CODE
const version = "dev"   // -X can't touch this
var version = "dev" // -X can set this






The value must be a plain string. If your variable is an int

and you try to inject a number, the linker leaves it alone. You

parse everything you inject as a string and convert later if you

need a number.



And the variable has to actually exist and be referenced enough

that the linker keeps it. Dead-code elimination can drop an

unused package-level var, and then your -X has no target. In

practice, printing it or exposing it through an endpoint is

enough to keep it alive.





Wiring in commit and build time



One version string is the start. What you want on that 2am box

is the commit hash and the build timestamp too. You pull those

from git and the shell at build time and pass them as three

separate -X flags.



Put the variables in their own package so the import path is

stable and the values aren't scattered across main:




CODE
package build

// Set via -ldflags -X at build time.
var (
Version = "dev"
Commit = "none"
Time = "unknown"
)






Then a Makefile target that gathers the values and builds the

flag string:




CODE
VERSION := $(shell git describe --tags --always)
COMMIT := $(shell git rev-parse --short HEAD)
TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
PKG := github.com/you/app/internal/build

LDFLAGS := -X $(PKG).Version=$(VERSION) \
-X $(PKG).Commit=$(COMMIT) \
-X $(PKG).Time=$(TIME)

build:
go build -ldflags "$(LDFLAGS)" -o app .






git describe --tags --always gives you the nearest tag when

there is one and falls back to the short hash when there isn't,

so untagged builds still produce something readable. The -u

on date keeps the timestamp in UTC, which is the only sane

choice for something that gets compared across machines.



Read it back wherever you expose version metadata:




CODE
package main

import (
"fmt"

"github.com/you/app/internal/build"
)

func main() {
fmt.Printf("version: %s\n", build.Version)
fmt.Printf("commit: %s\n", build.Commit)
fmt.Printf("built: %s\n", build.Time)
}









The half you already have for free



Here's the part most teams miss. Since Go 1.18, go build

stamps the git revision into every binary automatically. You

don't need -X for the commit at all. The toolchain reads your

VCS state and stores it in the build metadata.



You read it through runtime/debug:




CODE
package build

import "runtime/debug"

func VCSInfo() (rev string, dirty bool, ok bool) {
info, available := debug.ReadBuildInfo()
if !available {
return "", false, false
}
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
rev = s.Value
case "vcs.modified":
dirty = s.Value == "true"
}
}
return rev, dirty, rev != ""
}






The settings you care about are vcs.revision (the full commit

hash), vcs.time (the commit timestamp), and vcs.modified

(the string "true" when you built with uncommitted changes in

the tree). That last one is the honest signal you rarely add by

hand: it tells you the binary was built from a dirty checkout

and does not match any commit exactly.



debug.ReadBuildInfo() also hands you the module version and

the Go toolchain version:




CODE
info, _ := debug.ReadBuildInfo()
fmt.Println(info.GoVersion) // e.g. go1.24.0
fmt.Println(info.Main.Version) // module version






info.Main.Version is the interesting one. Build with

go install github.com/you/[email protected] and it reads v1.4.2.

Build locally with go build and it reads (devel), because a

local checkout has no released version to report.





When to reach for which



The two mechanisms overlap on the commit hash, so pick based on

what the value is.



Use the automatic BuildInfo VCS stamps for the commit,

commit time, and dirty flag. They cost you nothing, they can't

drift out of sync with the actual git state, and vcs.modified

catches builds you'd otherwise mislabel as clean.



Use -X for values git doesn't know: a semantic version you

compute from a tag plus build number, a release channel like

stable or canary, a build-pipeline ID. Those live outside

the repo, so the linker injection is the right tool.



A version package that reads from both ends up small:




CODE
func String() string {
rev, dirty, ok := VCSInfo()
if !ok {
rev = Commit // -X fallback
}
suffix := ""
if dirty {
suffix = "-dirty"
}
return fmt.Sprintf("%s (%s%s)",
Version, short(rev), suffix)
}

func short(s string) string {
if len(s) > 7 {
return s[:7]
}
return s
}









Gotchas that waste an afternoon



go run and go test don't record VCS stamps the way

go build and go install do, so vcs.revision comes back

empty in those. Test the real binary, not go run ., when you

verify version output.



Building outside the repository, or with -buildvcs=false,

skips the VCS stamping entirely. CI systems that copy source

into a directory without the .git folder hit this constantly.

If your commit hash is mysteriously empty in CI but fine

locally, check whether .git made it into the build context.



Stripping the binary is fine. The -w and -s linker flags

remove DWARF and the symbol table to shrink the file, and they

do not remove -X values or the BuildInfo block. You can ship

-ldflags "-s -w -X main.version=1.4.2" and still read the

version back at runtime.



And spaces in an injected value need quoting the linker

understands. Wrap the individual assignment, not the whole

flag string:




CODE
go build -ldflags \
"-X 'main.buildUser=Jane Doe'" -o app






None of this is exotic. It's four variables, a Makefile target,

and one function that reads BuildInfo. The payoff is that the

next time a binary answers --version, it tells you the tag,

the commit, and whether the tree was dirty, instead of shrugging

back dev.






Build-time injection is one of those Go details that stays

simple right up until you spread it across main, a Makefile,

and three environments. The Complete Guide to Go Programming

digs into the linker, runtime/debug, and how the toolchain

records build metadata. Hexagonal Architecture in Go is the

one to read when you want that version data behind a clean

boundary instead of leaking -ldflags details through your

whole codebase.



Thinking in Go — the 2-book series on Go programming and hexagonal architecture

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Modify Windows Support Phone Number with PowerShell
1 Quelle
Die Zukunft des Einkaufens: Warum wir ein neues Kapitel aufschlagen (und wie du es mitschreiben kannst)
1 Quelle
ZDE Podcast 251: Wie sieht digitales Instore Marketing 2026 aus, Amit Chatterjee?
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Injecting Version Info at Build Time in Go With -ldflags

Thematisch verwandte Begriffe: Injecting, Version, Info, Build · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...