Auto Build Numbering for Go: Solving the Versioning Problem Once and For All

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Manual Versioning is a Pain for Every Go Developer

\n

Have you ever caught yourself thinking: "What version is currently running in production?" or spent an hour trying to figure out which commit caused a bug in a binary built a month ago? If so, you've come to the right place. The question of "Application auto build versioning" is one of the most popular on Stack Overflow (thousands of views and dozens of solutions). And for good reason: in modern development, where CI/CD pipelines run builds every 10 minutes, manually setting versions is a path to chaos.

\n

In this article, we will break down automatic build versioning in Go from A to Z. You will learn how to implement a system that automatically sets the version number, commit hash, build time, and even a "dirty" repository flag. The code will work both in local development and in GitHub Actions, GitLab CI, or Jenkins.

\n\n

The Problem: What's Wrong with a Manual version.go?

\n

Many beginner developers create a version.go file and write:

\n
package main\n\nvar Version = "1.0.0"
\n

And this works exactly until the first commit. Then it starts:

\n
    \n
  • "I forgot to update the version before the build"
  • \n
  • "The version in production is one thing, in the code it's another"
  • \n
  • "Which commit corresponds to this build?"
  • \n
\n

The problem is that the version is build metadata, not part of the source code. It should be generated automatically at the compilation stage.

\n\n

The Solution: ldflags — Go's Secret Weapon

\n

The Go compiler allows you to "inject" variable values during the build using the -ldflags flag. This is auto build versioning in its classic form. We don't store the version in the code — we pass it to the compiler.

\n\n

Step 1: Define the Version Structure

\n

Let's create a version package with variables that will be filled at build time:

\n
// version/version.go\npackage version\n\nvar (\n    // Version — semantic version (set manually or via git tag)\n    Version = "dev"\n\n    // Commit — short commit hash\n    Commit = "none"\n\n    // BuildTime — build time in ISO 8601 format\n    BuildTime = "unknown"\n\n    // IsDirty — flag indicating the repository has uncommitted changes\n    IsDirty = "false"\n)\n\nfunc String() string {\n    return "v" + Version + " (commit: " + Commit + ", built: " + BuildTime + ")"\n}
\n\n

Step 2: Build with ldflags

\n

Now for the interesting part. Instead of go build, we will use a script or Makefile that calculates the required values and passes them to the compiler:

\n
#!/bin/bash\n# build.sh\n\nVERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")\nCOMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "none")\nBUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")\nIS_DIRTY=$(git diff --quiet || echo "true")\n\ngo build -ldflags="\\\n    -X 'main/version.Version=$VERSION' \\\n    -X 'main/version.Commit=$COMMIT' \\\n    -X 'main/version.BuildTime=$BUILD_TIME' \\\n    -X 'main/version.IsDirty=$IS_DIRTY'" \\\n    -o myapp .
\n

Important: The path main/version.Version depends on your project structure. If the version package is in the root, write main/version.Version. If it's in a separate directory, write github.com/your-username/project/version.Version.

\n\n

Step 3: Use in the Application

\n

Now you can output the version from anywhere in the program:

\n
// main.go\npackage main\n\nimport (\n    "fmt"\n    "main/version"\n)\n\nfunc main() {\n    fmt.Println("App version:", version.String())\n    fmt.Println("Is dirty repo:", version.IsDirty)\n}
\n

Build and run:

\n
$ ./build.sh\n$ ./myapp\nApp version: v1.2.3 (commit: a1b2c3d, built: 2024-03-15T10:30:00Z)\nIs dirty repo: false
\n\n

Advanced Level: Semantic

Blogs

Book Recommendations