Versioning Go Applications
Inject version information in Go applications using linker flags.
Overview
One of the must haves when it comes to running a Go application in production is to have a clear version number printed in the logs at the start of the application. This is helpful in the future when debugging a bug by looking at the logs, and seeing which version of the app caused this behavior.
Get Version
The version information can be stored as global variables with zero values, with the values being injected at build time by the linker.
Here is an example:
package main
import "log"
var APP_VERSION string
var APP_PRERELEASE string
var APP_BUILD string
func main() {
log.Println("Hello world", getVersionString())
}
func getVersionString() string {
version := ""
if len(APP_VERSION) > 0 {
version = APP_VERSION
} else {
version = "1.0.0"
}
if len(APP_PRERELEASE) > 0 {
version = version + "-" + APP_PRERELEASE
} else {
version = version + "-local.1"
}
if len(APP_BUILD) > 0 {
version = version + "+" + APP_BUILD
}
return version
}
Notice that the variables APP_VERSION
, APP_PRERELEASE
and APP_BUILD
are declared at the global scope, but left at the zero value (""
).
If you run this as is, you will see the following output:
$ go run main.go
2023/11/17 18:13:27 Hello world 1.0.0-local.1
Linker flags
As the next step, we will be injecting these variables at build time using linker flags:
$ go mod init github.com/msound/helloworld
$ go build -o helloworld -ldflags "-X 'main.APP_VERSION=1.0.0' -X 'main.APP_PRERELEASE=stage.a862776' -X 'main.APP_BUILD=build.422'"
This would build a binary named helloworld
which would have the version information baked into the binary. Now if we run the binary:
$ ./helloworld
2023/11/17 18:20:57 Hello world 1.0.0-stage.a862776+build.422
Dockerfile
You are probably going to run your Go app as a container, so you would want to pass these values through as arguments into a Dockerfile. Here’s how you would do it:
FROM golang:1.21 AS build-env
ENV GOOS=linux GOARCH=amd64 CGO_ENABLED=0
ARG APP_VERSION
ARG APP_PRERELEASE
ARG APP_BUILD
WORKDIR /app
COPY . /app
RUN go build \
-o helloworld \
-ldflags "-X 'main.APP_VERSION=${APP_VERSION}' -X 'main.APP_PRERELEASE=${APP_PRERELEASE}' -X 'main.APP_BUILD=${APP_BUILD}'"
###########################
FROM alpine:3.11
RUN apk update && \
apk add --no-cache --purge ca-certificates curl tzdata && \
rm -rf /var/cache/apk/* /tmp/*
RUN adduser -D appuser --uid 3000
COPY --from=build-env /app/helloworld /app/helloworld
USER appuser
CMD ["/app/helloworld"]
The Dockerfile receives the version information as ARGs, and passes them on to go build
as linker arguments.
CI/CD Pipeline
The final step is to pass the arguments to docker build
from the CI/CD Pipeline. Here is the snippet of this for Gitlab. For Github actions, you can adapt this as needed:
script:
- |
export APP_VERSION=${CI_COMMIT_TAG}
export APP_PRERELEASE=stage.${CI_COMMIT_SHORT_SHA}
export APP_BUILD=build.${CI_JOB_ID}
docker build \
--build-arg "APP_VERSION=${APP_VERSION}" \
--build-arg "APP_PRERELEASE=${APP_PRERELEASE}" \
--build-arg "APP_BUILD=${APP_BUILD}" \
-t helloworld:${CI_COMMIT_TAG} .