Sr Technical Content Strategist and Team Lead
Using ldflags to set version information for Go applications is a standard practice for embedding build metadata into compiled binaries. The -ldflags flag in go build passes instructions to the Go linker (cmd/link), letting you set the value of string variables at compile time without modifying source code. This means you can inject a version number, Git commit hash, build timestamp, or any other string into your binary directly from the command line or a CI/CD pipeline.
When deploying applications into production, this version metadata improves monitoring, logging, and debugging by giving you exact build traceability. Without it, identifying which code is running on a given server requires manual tracking that is tedious and error-prone. The ldflags -X approach solves this by writing values into package-level string variables during the link phase, producing a self-describing binary with zero runtime cost.
In this tutorial, you will build a Go application that prints version, build time, and Git commit information. You will use -ldflags -X to inject those values at build time, target variables in sub-packages, add a --version CLI flag, automate the build with a Makefile and GitHub Actions, and compare the ldflags approach with debug.ReadBuildInfo (available since Go 1.18). By the end, you will have a reusable build pipeline pattern for embedding version info into any Go project.
-ldflags "-X 'package.Variable=value'" syntax sets package-level string variables at link time without changing source code.string type variables at the package level can be set with -X. Constants, non-string types, and function-initialized variables are not supported.$(git rev-parse --short HEAD) and $(date -u +%Y-%m-%dT%H:%M:%SZ) to inject dynamic build metadata.debug.ReadBuildInfo(), which works without any ldflags configuration.ldflags injection so every build gets consistent, accurate version information.ldflags approach works with all Go versions and gives you explicit control, while debug.ReadBuildInfo requires Go 1.18+ and depends on VCS state being available at build time.To follow the examples in this tutorial, you will need:
go build command. If you are new to building Go programs, read How To Build and Install Go Programs first.The -ldflags flag passes arguments to the Go linker during go build. The ld in ldflags stands for linker, and the flags control how the linker assembles the final binary. The most commonly used linker flag is -X, which writes a value into a string variable at link time.
The full syntax looks like this:
- go build -ldflags="-X 'package_path.VariableName=value'"
The double quotes wrap the entire ldflags string. Inside, -X takes a single argument in the format package_path.VariableName=value. The package path is the full Go import path to the package containing the variable. The variable must be a package-level string variable (not a constant, not a non-string type, and not initialized by a function call).
For -X to work, the target variable must meet these conditions:
string.Version and version work).const, because constants are inlined at compile time and have no runtime address for the linker to write to.var Version = getVersion().Wrong package path: If you pass an incorrect package path, the build succeeds silently but the variable keeps its default value. Always verify the import path in your go.mod file or use go tool nm to inspect the binary (shown later in this tutorial).
Using const instead of var: The linker cannot modify constants. Change const Version = "dev" to var Version = "dev".
Missing quotes around the value: If your value contains spaces (like a date string), wrap the entire -X argument in single quotes:
- go build -ldflags="-X 'main.BuildTime=2026-04-06 14:30:00'"
In this step, you will create a Go module with a version variable and build it with a static default value.
Create a new project directory and initialize a Go module:
- mkdir go-version-example
- cd go-version-example
- go mod init go-version-example
Create the entry point main.go:
package main
import (
"fmt"
)
var Version = "development"
func main() {
fmt.Println("Version:\t", Version)
}
The Version variable defaults to "development". This default value appears when you build without -ldflags, making it clear during local development that the binary was not built through a release process.
Build and run the application:
- go build -o app
- ./app
You will see:
OutputVersion: development
Now build again with -ldflags to set the version:
- go build -o app -ldflags="-X 'main.Version=v1.0.0'"
- ./app
The output now shows the injected version:
OutputVersion: v1.0.0
The main in main.Version is the package path. Since Version lives in the main package, the path is simply main. For variables in other packages, you use the full import path, which is covered in a later step.
For more on how go build works and its options, see How To Build and Install Go Programs.
A version string alone does not tell you the exact source code state behind a binary. Injecting the Git commit hash and build timestamp gives you full traceability.
Update main.go to include additional variables:
package main
import (
"fmt"
)
var (
Version = "development"
CommitSHA = "none"
BuildTime = "unknown"
)
func main() {
fmt.Println("Version:\t", Version)
fmt.Println("Commit:\t\t", CommitSHA)
fmt.Println("Built:\t\t", BuildTime)
}
Build with shell substitution to capture the Git hash and current UTC time:
- go build -o app -ldflags="-X 'main.Version=v1.0.0' -X 'main.CommitSHA=$(git rev-parse --short HEAD)' -X 'main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
git rev-parse --short HEAD returns the abbreviated commit hash (7 characters). date -u +%Y-%m-%dT%H:%M:%SZ produces a UTC timestamp in ISO 8601 format, which avoids spaces and time zone ambiguity.
Run the binary:
- ./app
You will see output similar to:
OutputVersion: v1.0.0
Commit: a1b2c3d
Built: 2026-04-06T14:30:00Z
The exact commit hash and timestamp will reflect your repository state and build time.
Users and operators expect a --version flag on CLI tools. Go’s standard library flag package makes this straightforward.
Update main.go:
package main
import (
"flag"
"fmt"
"os"
)
var (
Version = "development"
CommitSHA = "none"
BuildTime = "unknown"
)
func main() {
showVersion := flag.Bool("version", false, "Print version information and exit")
flag.Parse()
if *showVersion {
fmt.Printf("Version: %s\nCommit: %s\nBuilt: %s\n", Version, CommitSHA, BuildTime)
os.Exit(0)
}
fmt.Println("Application is running...")
}
Build with ldflags and test:
- go build -o app -ldflags="-X 'main.Version=v1.0.0' -X 'main.CommitSHA=$(git rev-parse --short HEAD)' -X 'main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
- ./app --version
OutputVersion: v1.0.0
Commit: a1b2c3d
Built: 2026-04-06T14:30:00Z
Running ./app without the flag starts the application normally.
In real projects, version information often lives in a dedicated package rather than main, so it can be imported by multiple parts of the application (HTTP handlers, logging middleware, health checks). The -X flag requires the full import path to reach variables outside the main package.
Create a version sub-package:
- mkdir version
Create version/version.go:
package version
var (
Version = "development"
CommitSHA = "none"
BuildTime = "unknown"
)
Update main.go to import and use this package:
package main
import (
"flag"
"fmt"
"os"
"go-version-example/version"
)
func main() {
showVersion := flag.Bool("version", false, "Print version information and exit")
flag.Parse()
if *showVersion {
fmt.Printf("Version: %s\nCommit: %s\nBuilt: %s\n",
version.Version, version.CommitSHA, version.BuildTime)
os.Exit(0)
}
fmt.Println("Application is running...")
}
The -X flag now needs the full import path go-version-example/version:
- go build -o app -ldflags="-X 'go-version-example/version.Version=v1.0.0' -X 'go-version-example/version.CommitSHA=$(git rev-parse --short HEAD)' -X 'go-version-example/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
- ./app --version
OutputVersion: v1.0.0
Commit: a1b2c3d
Built: 2026-04-06T14:30:00Z
If you are unsure of the exact import path for a variable, build the binary first and then inspect it:
- go build -o app
- go tool nm ./app | grep version
The output will include lines like:
Output 55d2c0 D go-version-example/version.BuildTime
55d2d0 D go-version-example/version.CommitSHA
55d2e0 D go-version-example/version.Version
The D indicates a data symbol. The full path before the . is what you pass to -X.
Note: go tool nm does not support package names containing non-ASCII characters, ", or % characters.
Typing the full ldflags string on every build is repetitive and error-prone. A Makefile captures the build command in a single reusable target.
Create a Makefile in the project root:
VERSION ?= $(shell git describe --tags --always --dirty)
COMMIT := $(shell git rev-parse --short HEAD)
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
PKG := go-version-example/version
LDFLAGS := -X '$(PKG).Version=$(VERSION)' \
-X '$(PKG).CommitSHA=$(COMMIT)' \
-X '$(PKG).BuildTime=$(BUILD_TIME)'
.PHONY: build clean
build:
go build -o app -ldflags="$(LDFLAGS)"
clean:
rm -f app
git describe --tags --always --dirty produces a human-readable version from the nearest Git tag. If the working tree has uncommitted changes, it appends -dirty. The ?= operator lets you override VERSION from the command line: make build VERSION=v2.0.0.
Run the build:
- make build
- ./app --version
For automated releases, add a GitHub Actions workflow that injects version information from the Git tag:
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26'
- name: Build binary
run: |
VERSION=${{ github.ref_name }}
COMMIT=$(git rev-parse --short HEAD)
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
PKG=go-version-example/version
go build -o app -ldflags="-X '${PKG}.Version=${VERSION}' -X '${PKG}.CommitSHA=${COMMIT}' -X '${PKG}.BuildTime=${BUILD_TIME}'"
- name: Verify version
run: ./app --version
This workflow triggers when you push a Git tag that starts with v (for example, v1.2.0). The github.ref_name context variable contains the tag name, which becomes the version string. Each binary built this way carries the exact tag, commit, and build time from the CI environment.
Starting with Go 1.12, the runtime/debug package provides debug.ReadBuildInfo(), which returns metadata about the running binary without any ldflags setup. Go 1.18 expanded this to include VCS (Version Control System) information when the binary is built inside a Git repository.
Create a standalone example to see what debug.ReadBuildInfo provides:
package main
import (
"fmt"
"runtime/debug"
)
func main() {
info, ok := debug.ReadBuildInfo()
if !ok {
fmt.Println("Build info not available.")
return
}
fmt.Println("Go version:", info.GoVersion)
fmt.Println("Module path:", info.Path)
fmt.Println("Module version:", info.Main.Version)
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
fmt.Println("VCS revision:", setting.Value)
case "vcs.time":
fmt.Println("VCS time:", setting.Value)
case "vcs.modified":
fmt.Println("VCS modified:", setting.Value)
}
}
}
Save this as buildinfo_example.go in your project directory (replacing main.go temporarily or renaming the file). Build and run it inside the Git repository. Use go build . (not go build buildinfo_example.go) so that Go includes VCS metadata:
- go build -o buildinfo .
- ./buildinfo
OutputGo version: go1.26
Module path: go-version-example
Module version: (devel)
VCS revision: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
VCS time: 2026-04-06T12:00:00Z
VCS modified: false
The vcs.revision key contains the full commit hash, vcs.time is the commit timestamp in RFC 3339 format, and vcs.modified indicates whether the working tree had uncommitted changes.
debug.ReadBuildInfo vs ldflagsUse debug.ReadBuildInfo when:
Use ldflags -X when:
v2.1.0) that differs from the Git state.In many production setups, teams use both: ldflags for the release version string and debug.ReadBuildInfo for supplementary build metadata.
The following table compares three approaches for embedding version information in Go binaries:
| Feature | ldflags -X |
debug.ReadBuildInfo |
Config file (embedded or external) |
|---|---|---|---|
| Go version required | All versions | Go 1.18+ for VCS info | All versions |
| Setup required | Build script or Makefile | None (automatic) | File generation step |
| Custom version string | Yes (any string) | Limited to Git tag/pseudo-version | Yes (any format) |
| Git commit hash | Manual via shell substitution | Automatic (vcs.revision) |
Manual generation |
| Build timestamp | Manual via shell substitution | Automatic (vcs.time) |
Manual generation |
| Works without VCS | Yes | No VCS fields without a repository | Yes |
| Runtime overhead | None | Minimal (reads embedded data) | File I/O if external |
| Binary size impact | Negligible (static strings) | Negligible (already embedded by Go) | Adds file content |
| CI/CD compatibility | Works everywhere | Requires VCS state in build env | Works everywhere |
For most Go projects, ldflags -X is the most portable and widely supported approach. debug.ReadBuildInfo is a good supplement when you want automatic metadata without maintaining build scripts. Config files are useful when version info needs to be shared with non-Go components (for example, a frontend that reads a version.json file).
In production, version information is often served from a /version or /healthz endpoint so monitoring tools and load balancers can verify which build is running.
Create server.go:
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"go-version-example/version"
)
func main() {
showVersion := flag.Bool("version", false, "Print version information and exit")
flag.Parse()
if *showVersion {
fmt.Printf("Version: %s\nCommit: %s\nBuilt: %s\n",
version.Version, version.CommitSHA, version.BuildTime)
os.Exit(0)
}
http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"version": version.Version,
"commit": version.CommitSHA,
"buildTime": version.BuildTime,
})
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from go-version-example %s", version.Version)
})
log.Println("Starting server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Build and run the server:
- go build -o server -ldflags="-X 'go-version-example/version.Version=v1.0.0' -X 'go-version-example/version.CommitSHA=$(git rev-parse --short HEAD)' -X 'go-version-example/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
- ./server
In another terminal, query the version endpoint:
- curl http://localhost:8080/version
Output{"buildTime":"2026-04-06T14:30:00Z","commit":"a1b2c3d","version":"v1.0.0"}
This pattern is used by major Go projects including Kubernetes and Prometheus to expose build metadata at runtime.
-ldflags -X in Go?Only package-level variables of type string can be set. The variable must be declared with var, not const. It can be exported or unexported, but it cannot have its value initialized by a function call like var Version = getVersion(). Non-string types (int, bool, struct) are not supported.
-ldflags -X to set variables in packages other than main?Yes. Use the full import path of the package in the -X argument. For example, if your module is github.com/user/repo and the variable is in the version package, use: -X 'github.com/user/repo/version.Version=1.0.0'.
-ldflags -X does not match any variable in the binary?The build succeeds without any error or warning, but the variable retains its default value. This is a silent failure. During development, always print the injected variables at startup to verify they were set correctly.
Use shell command substitution in the build command: go build -ldflags "-X main.CommitSHA=$(git rev-parse --short HEAD)". This captures the abbreviated 7-character commit hash and embeds it in the binary.
-ldflags -X and debug.ReadBuildInfo?-ldflags -X requires you to pass values explicitly at build time, giving you full control over what is embedded. debug.ReadBuildInfo() reads metadata automatically from the Go module system and VCS state (Go 1.18+). The ldflags approach works with all Go versions and does not depend on a VCS repository being present. debug.ReadBuildInfo provides automatic VCS metadata but only works when the binary is built inside a Git repository with module mode enabled.
-ldflags affect binary size or runtime performance?The impact on binary size is negligible. The injected strings are stored as static data in the binary, adding only the number of bytes in the string values themselves. There is no measurable runtime performance impact because the values are read as regular Go variables.
-ldflags -X with go install?Yes. The same syntax works: go install -ldflags "-X main.Version=1.2.0" ./.... The resulting binary installed to $GOPATH/bin will contain the injected values.
-ldflags in a GitHub Actions workflow for automated releases?Define the version from the Git tag using ${{ github.ref_name }} and pass it to go build. A complete example is provided in the Integrating into a GitHub Actions Workflow section of this tutorial.
In this tutorial, you used -ldflags -X to inject version information, Git commit hashes, and build timestamps into Go binaries at build time. You built a sample application with a --version CLI flag, organized version variables in a dedicated sub-package, automated the build with a Makefile and GitHub Actions, and compared the ldflags approach with debug.ReadBuildInfo from the runtime/debug package.
Adding version metadata to your build workflow gives you reliable traceability across development, staging, and production environments. The ldflags pattern is straightforward, works with every Go version, and integrates into any CI/CD pipeline.
If you would like to learn more about the Go programming language, check out the full How To Code in Go series. For cross-platform builds, see Building Go Applications for Different Operating Systems and Architectures. To learn about Go’s package initialization order, read Understanding init in Go.
<$>[info]
Build and deploy on DigitalOcean: Use DigitalOcean App Platform to deploy your Go applications directly from a GitHub repository with automatic builds and managed infrastructure. You can pass ldflags in your build command configuration. Get started with App Platform today.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
Go (or GoLang) is a modern programming language originally developed by Google that uses high-level syntax similar to scripting languages. It is popular for its minimal syntax and innovative handling of concurrency, as well as for the tools it provides for building native binaries on foreign platforms.
Browse Series: 53 tutorials
I help Businesses scale with AI x SEO x (authentic) Content that revives traffic and keeps leads flowing | 3,000,000+ Average monthly readers on Medium | Sr Technical Writer(Team Lead) @ DigitalOcean | Ex-Cloud Consultant @ AMEX | Ex-Site Reliability Engineer(DevOps)@Nutanix
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
There’s nothing incorrect about this, but it’s not a very Go-ish way to go about it.
Rather than using ldflags you can use go generate to create the desired file.
See https://www.reddit.com/r/golang/comments/hso4zb/add_semver_based_on_git_describe_easy_as_1_2_3/
The basic idea is that if you had a package github.com/foobar/foobar with a main.go like this:
//go:generate go github.com/foobar/foobar/tools/genversion.go
package main
var Version string
func main() {
fmt.Printf("foobar v%s\n" + Version)
// ...
}
Then your tools/genversion.go would template a file like this:
package main
func init() {
Version = "v1.3.1"
}
You can see an example of such a genversion.go here: https://git.rootprojects.org/root/go-gitver/src/branch/master/gitver.go
also, if you are using modules: as of go1.18, the go command automatically adds your module’s semver to your binaries. ( https://tip.golang.org/doc/go1.18#go-version. )
you can then use package debug to extract it ( https://pkg.go.dev/runtime/debug#ReadBuildInfo ):
import "runtime/debug"
func GetVersion() (ret string) {
if b, ok := debug.ReadBuildInfo(); ok && len(b.Main.Version) > 0 {
ret= b.Main.Version
} else {
ret= "unknown"
}
return
}
ReadBuildInfo() can return false if not using modules; and Main.Version can be empty if you’re building files individually. ( ex. go build app.go instead of simply go build )
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.