Why Manual Versioning is a Pain for Every Go Developer
\nHave 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.
\nIn 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\nThe Problem: What's Wrong with a Manual version.go?
\nMany beginner developers create a version.go file and write:
package main\n\nvar Version = "1.0.0"\nAnd 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
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\nThe Solution: ldflags — Go's Secret Weapon
\nThe 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.
Step 1: Define the Version Structure
\nLet's create a version package with variables that will be filled at build time:
// 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\nStep 2: Build with ldflags
\nNow 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:
#!/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 .\nImportant: 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.
Step 3: Use in the Application
\nNow 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}\nBuild 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