Embedding a build number in a Go executable

Embedding a build number in a Go executable

part of Go Cookbook
So you’ve deployed your web application to production and it’s running on a server far, far away.
When debugging problems it’s good to know what version of the code is running.
If you’re using Git, the commit ID tells you which revision was used to build the program.

Read automatically embedded build information

Since Go 1.18, the Go command can embed VCS metadata when building a main package from a supported version-control checkout. Build the package with go build ., rather than naming individual source files. Use -buildvcs=true to fail if available VCS information cannot be included, or -buildvcs=false to disable stamping. See the Go command release notes.
Inspect a built executable without running it:
go version -m ./myapp
Inside the program, use debug.ReadBuildInfo:
func printBuildInfo() {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		fmt.Println("build information unavailable")
		return
	}
	fmt.Printf("Go: %s\nmodule: %s %s\n", info.GoVersion, info.Main.Path, info.Main.Version)
	for _, setting := range info.Settings {
		switch setting.Key {
		case "vcs.revision", "vcs.time", "vcs.modified":
			fmt.Printf("%s: %s\n", setting.Key, setting.Value)
		}
	}
}
This helper imports fmt and runtime/debug. VCS fields can be absent, for example when building outside a checkout. vcs.time is the commit time, not the build time, and vcs.modified indicates uncommitted changes. A source revision and a clean-tree indicator are often more useful than a build timestamp alone.

Set a custom version with linker flags

We can embed that version in the executable during the build thanks to the Go linker’s -X option, which sets a string variable that is uninitialized or initialized with a constant string expression. It cannot set arbitrary variables or constants. See the linker documentation.
In our Go program we would have:
package main

var (
	sha1ver   string // sha1 revision used to build the program
	buildTime string // when the executable was built
)
We can set that variable to the Git commit ID in our build script build.sh:
#!/bin/bash

# notice how we avoid spaces in $now to avoid quotation hell in go build command
now=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
go build -ldflags "-X main.sha1ver=$(git rev-parse HEAD) -X main.buildTime=$now" .
Full example: embed-build-number/build.sh
On Windows we would write a PowerShell script build.ps1:
# notice how we avoid spaces in $now to avoid quotation hell in go build command
$now = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$sha1 = (git rev-parse HEAD).Trim()

go build -ldflags "-X main.sha1ver=$sha1 -X main.buildTime=$now" .
Full example: embed-build-number/build.ps1
Let’s deconstruct:
We also need an easy way to see that version. We can add a -version command-line flag to print it out:
var (
	flgVersion bool
)

func parseCmdLineFlags() {
	flag.BoolVar(&flgVersion, "version", false, "if true, print version and exit")
	flag.Parse()
	if flgVersion {
		fmt.Printf("Built on %s from sha1 %s\n", buildTime, sha1ver)
		os.Exit(0)
	}
}
If this is a web application, we can additionally add a debug page that would show the version. I often do it like that:
func servePlainText(w http.ResponseWriter, s string) {
	w.Header().Set("Content-Type", "text/plain")
	w.Header().Set("Content-Length", strconv.Itoa(len(s)))
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(s))
}

// /app/debug
func handleDebug(w http.ResponseWriter, r *http.Request) {
	s := fmt.Sprintf("url: %s %s", r.Method, r.RequestURI)
	a := []string{s}

	a = append(a, "")
	a = append(a, fmt.Sprintf("ver: https://github.com/kjk/the-code/commit/%s", sha1ver))
	a = append(a, fmt.Sprintf("built on: %s", buildTime))

	s = strings.Join(a, "\n")
	servePlainText(w, s)
}

func makeHTTPServer() *http.Server {
	mux := &http.ServeMux{}

	mux.HandleFunc("/app/debug", handleDebug)

	return &http.Server{
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 5 * time.Second,
		IdleTimeout:  120 * time.Second,
		Handler:      mux,
	}
}

func startHTTPServer() {
	httpAddr := "127.0.0.1:4040"
	httpSrv := makeHTTPServer()
	httpSrv.Addr = httpAddr
	fmt.Printf("Visit http://%s/app/debug\n", httpAddr)
	err := httpSrv.ListenAndServe()
	if err != nil {
		log.Fatalf("httpSrv.ListenAndServe() failed with %s\n", err)
	}
}
Expose only the build details you need. Keep a debug endpoint private or authenticated, and avoid dumping request headers: they can contain cookies and authorization credentials.
Code for this chapter: https://github.com/kjk/the-code/tree/master/go/embed-build-number
#go
Sep 5 2026

Related articles

Feedback about page:

Feedback:
Optional: your email if you want me to get back to you: