Report this

What is the reason for this report?

Using ldflags to Set Version Information for Go Applications

Updated on April 8, 2026
Anish Singh Walia

By Anish Singh Walia

Sr Technical Content Strategist and Team Lead

English
Using ldflags to Set Version Information for Go Applications

Introduction

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.

Key Takeaways

  • The -ldflags "-X 'package.Variable=value'" syntax sets package-level string variables at link time without changing source code.
  • Only string type variables at the package level can be set with -X. Constants, non-string types, and function-initialized variables are not supported.
  • Use shell substitution like $(git rev-parse --short HEAD) and $(date -u +%Y-%m-%dT%H:%M:%SZ) to inject dynamic build metadata.
  • Go 1.18+ automatically embeds VCS metadata accessible through debug.ReadBuildInfo(), which works without any ldflags configuration.
  • A Makefile or GitHub Actions workflow can automate ldflags injection so every build gets consistent, accurate version information.
  • The 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.

Prerequisites

To follow the examples in this tutorial, you will need:

Understanding the -X Linker Flag

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:

  1. 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).

Variable Declaration Rules

For -X to work, the target variable must meet these conditions:

  • It must be a package-level variable (not declared inside a function).
  • It must be of type string.
  • It can be exported or unexported (both Version and version work).
  • It cannot be a const, because constants are inlined at compile time and have no runtime address for the linker to write to.
  • It cannot be initialized by a function call like var Version = getVersion().

Common Mistakes and How to Avoid Them

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:

  1. go build -ldflags="-X 'main.BuildTime=2026-04-06 14:30:00'"

Step 1: Setting Up a Go Project for Version Injection

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:

  1. mkdir go-version-example
  1. cd go-version-example
  1. go mod init go-version-example

Create the entry point main.go:

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:

  1. go build -o app
  1. ./app

You will see:

Output
Version: development

Now build again with -ldflags to set the version:

  1. go build -o app -ldflags="-X 'main.Version=v1.0.0'"
  1. ./app

The output now shows the injected version:

Output
Version: 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.

Step 2: Injecting Git Commit Hash and Build Timestamp

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:

main.go
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:

  1. 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:

  1. ./app

You will see output similar to:

Output
Version: 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.

Displaying Version Info via a --version CLI Flag

Users and operators expect a --version flag on CLI tools. Go’s standard library flag package makes this straightforward.

Update main.go:

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:

  1. 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)'"
  1. ./app --version
Output
Version: v1.0.0 Commit: a1b2c3d Built: 2026-04-06T14:30:00Z

Running ./app without the flag starts the application normally.

Step 3: Using ldflags with Packages Outside of main

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:

  1. mkdir version

Create version/version.go:

version/version.go
package version

var (
 Version   = "development"
 CommitSHA = "none"
 BuildTime = "unknown"
)

Update main.go to import and use this package:

main.go
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:

  1. 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)'"
  1. ./app --version
Output
Version: v1.0.0 Commit: a1b2c3d Built: 2026-04-06T14:30:00Z

Finding the Correct Package Path with go tool nm

If you are unsure of the exact import path for a variable, build the binary first and then inspect it:

  1. go build -o app
  1. 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.

Step 4: Automating Version Injection with a Makefile

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:

Makefile
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:

  1. make build
  1. ./app --version

Integrating into a GitHub Actions Workflow

For automated releases, add a GitHub Actions workflow that injects version information from the Git tag:

.github/workflows/release.yml
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.

Step 5: Alternative Approach with debug.ReadBuildInfo

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:

buildinfo_example.go
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:

  1. go build -o buildinfo .
  1. ./buildinfo
Output
Go 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.

When to Use debug.ReadBuildInfo vs ldflags

Use debug.ReadBuildInfo when:

  • You want automatic VCS metadata without configuring build scripts.
  • Your binary is always built from a clean Git repository.
  • You do not need a custom version string separate from the Git tag.

Use ldflags -X when:

  • You need to inject a specific release version (like v2.1.0) that differs from the Git state.
  • Your build environment does not have VCS access (for example, building from a source tarball).
  • You need to support Go versions before 1.18.
  • You want full control over exactly what metadata is embedded.

In many production setups, teams use both: ldflags for the release version string and debug.ReadBuildInfo for supplementary build metadata.

Comparison of Version Injection Methods

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).

Practical Example: Exposing Version Info in an HTTP Server

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:

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:

  1. 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)'"
  1. ./server

In another terminal, query the version endpoint:

  1. 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.

FAQs

1. What types of variables can be set using -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.

2. Can I use -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'.

3. What happens if the variable name in -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.

4. How do I inject the current Git commit hash into a Go binary at build time?

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.

5. What is the difference between -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.

6. Does using -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.

7. Can I use -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.

8. How do I use -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.

Conclusion

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.

Learn more about our products

Tutorial Series: How To Code in Go

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.

About the author

Anish Singh Walia
Anish Singh Walia
Author
Sr Technical Content Strategist and Team Lead
See author profile

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

Category:

Still looking for an answer?

Was this helpful?


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 )

Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Start building today

From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.