Avatar ·

Как добавить номер версии и хэш коммита в Go-приложение при сборке?

Всем привет! Я пишу небольшое веб-приложение на Go и хочу, чтобы при запуске оно выводило свою версию. Сейчас я просто вручную меняю константу в коде: ```go const Version = "1.0.3" ``` Но это ужасно неудобно. При каждом коммите приходится лезть в код, менять строку, коммитить отдельно. А еще хочется видеть не только номер версии, но и хэш коммита, и дату сборки — чтобы точно знать, какая сборка запущена на сервере. Пробовал читать файлы, но это не работает, если бинарник перенести на другую машину. Слышал про ldflags, но не могу разобраться, как их правильно использовать. Расскажите, как принято делать версионирование в Go-проектах? Как передать версию, хэш коммита и дату сборки в бинарник на этапе компиляции, чтобы это работало без костылей? И чтобы при запуске `./myapp version` или `./myapp --version` выводилась полная информация. Буду благодарен за пример Makefile или скрипта сборки, который делает это автоматически при CI/CD. <<>>

Использование 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 — он чище и надёжнее.