Как добавить номер версии и хэш коммита в Go-приложение при сборке?
Использование ldflags для передачи метаданных сборки
В Go нет встроенного механизма версионирования, но есть элегантный способ передать значения в глобальные переменные на этапе компиляции — флаг -ldflags -X. Это стандартный подход в Go-сообществе, который использует большинство проектов (Hugo, Docker, Kubernetes и др.).
Шаг 1: Определяем переменные в коде
Создайте файл version.go в корне проекта:
package main
import (
"fmt"
"runtime"
"time"
)
// Переменные заполняются через ldflags при сборке
var (
version = "dev"
commit = "none"
buildDate = "unknown"
)
// PrintVersion выводит информацию о сборке
func PrintVersion() {
fmt.Printf("Version: %s\n", version)
fmt.Printf("Commit: %s\n", commit)
fmt.Printf("BuildDate: %s\n", buildDate)
fmt.Printf("GoVersion: %s\n", runtime.Version())
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
Шаг 2: Добавляем флаг --version в main.go
package main
import (
"flag"
"log"
"net/http"
)
func main() {
showVersion := flag.Bool("version", false, "Show version info")
flag.Parse()
if *showVersion {
PrintVersion()
return
}
// ... остальной код приложения
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Шаг 3: Собираем с ldflags
Теперь самое главное — передать реальные значения при сборке:
go build -ldflags="\
-X main.version=1.0.3 \
-X main.commit=$(git rev-parse --short HEAD) \
-X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o myapp main.go
После этого запуск ./myapp --version выведет:
Version: 1.0.3
Commit: a1b2c3d
BuildDate: 2025-03-17T14:30:00Z
GoVersion: go1.22
OS/Arch: linux/amd64
Шаг 4: Автоматизация через Makefile
Создайте Makefile в корне проекта:
APP_NAME := myapp
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -ldflags="-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(DATE)"
.PHONY: build clean
build:
@echo "Building $(APP_NAME) version $(VERSION)..."
go build $(LDFLAGS) -o bin/$(APP_NAME) .
clean:
rm -rf bin/
run: build
./bin/$(APP_NAME)
Шаг 5: Интеграция с CI/CD (GitHub Actions)
Пример для автоматической сборки при пуше тега:
name: Build and 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.22'
- name: Build
run: |
VERSION=${GITHUB_REF_NAME#v}
COMMIT=${{ github.sha }}
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
go build -ldflags="-X main.version=$VERSION -X main.commit=$COMMIT -X main.buildDate=$DATE" -o myapp .
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: myapp-linux
path: myapp
Важные моменты:
- Всегда используйте
main.перед именем переменной — это указывает путь к пакету - Переменные должны быть глобальными (
var, неconst) —constнельзя переопределить через ldflags - Для Windows используйте
$env:DATE = ...или вызывайтеcmd /c "date /t" - Для Docker-сборок передавайте значения через
--build-arg
Этот подход даёт детерминированные сборки: вы всегда точно знаете, какой код запущен на сервере, и можете сопоставить версию с тегом в репозитории.
<<Альтернатива: внедрение через файл версии и go:embed
Если вы не хотите усложнять команду сборки, можно использовать генерацию файла версии на этапе сборки и встраивание его через //go:embed (доступно с Go 1.16).
Создайте скрипт generate_version.sh:
#!/bin/bash
cat > version_info.go <<EOF
package main
import _ "embed"
//go:embed .version
var embeddedVersion string
func getVersion() string {
return embeddedVersion
}
EOF
echo "$(git describe --tags --always) $(git rev-parse --short HEAD) $(date -u +%Y-%m-%d)" > .version
Затем в main.go используйте функцию getVersion(). При каждой сборке запускайте bash generate_version.sh && go build.
Плюсы:
- Не нужно помнить сложные ldflags
- Легко отлаживать — файл .version можно посмотреть
- Работает со старыми версиями Go (если не использовать embed)
Минусы:
- Требует дополнительного шага перед сборкой
- Можно забыть перегенерировать файл
- Меньше контроля над форматированием даты
Для небольших проектов или прототипов этот подход проще. Но для серьёзных продуктов рекомендую первый вариант с ldflags — он чище и надёжнее.