golang

Как управлять сборками в Go с помощью build tags

  • четверг, 6 марта 2025 г. в 00:00:13
https://habr.com/ru/companies/otus/articles/886044/

Привет, Хабр!

Вы когда‑нибудь сталкивались с ситуацией, когда нужно собрать Go‑приложение под несколько платформ? Или выключить часть кода в проде, оставив её активной в дев‑среде? Возможно, вы просто хотите поддерживать разные версии сборки с кастомными фичами без тонны if runtime.GOOS == «windows» {}?

В этом вам помогут build tags.

Build tags в Go — это специальные комментарии, которые говорят компилятору:
«Этот файл включаем в сборку только если выполнены вот эти условия».

Вот как они выглядят:

//go:build linux

или (по старинке, но так писать уже не стоит):

// +build linux

Когда и как использовать build tags?

Платформозависимый код

Допустим, есть приложение, которое должно работать на Windows и Linux, но кое‑где мы используем системные вызовы, которые не совместимы между платформами.

Решение? Два отдельных файла, каждый со своим build tag.

file_windows.go:

//go:build windows

package main

import "fmt"

func SayHello() {
    fmt.Println("Привет, Windows!")
}

file_linux.go:

//go:build linux

package main

import "fmt"

func SayHello() {
    fmt.Println("Привет, Linux!")
}

Теперь, когда делаем go build на Windows, в сборку попадёт только file_windows.go, а на Linux — file_linux.go. Всё, никакого if runtime.GOOS == «windows» — просто чистый код.

Скрытие отладочного кода в проде

Иногда хорошим решением будет добавлять «чёрный ход» в дев‑сборку, например, логирование того, чего в проде видеть не надо.

//go:build debug

package main

import "fmt"

func debugLog(msg string) {
    fmt.Println("[DEBUG]:", msg)
}

Собираем так:

go build -tags debug

Если не передать ‑tags debug, этот файл не попадёт в билд.

Несколько тегов одновременно

Если нужно, чтобы код работал только на Linux и только в debug‑сборке, пишем так:

//go:build linux && debug

Если подойдёт хоть одно из условий (или Linux, или debug), используем ||:

//go:build linux || debug

Чистенько, без if и без костылей.

Исключение файлов из сборки

Допустим, есть файл, который никогда не должен попасть в релизную версию. Например, что‑то экспериментальное.

Добавляем:

//go:build ignore

Этот файл просто не скомпилируется, пока мы сами не скажем обратное.

Build tags и cgo

Go и так обожает спорить с cgo, а если нужно ограничить использование нативных библиотек только для определённой платформы, это выглядит вот так:

//go:build linux && cgo

Теперь этот файл соберётся только если:

  • Компиляция идёт под Linux

  • Включён cgo (CGO_ENABLED=1)

Возможные проблемы

//go:build должен быть ПЕРВЫМ в файле. Компилятор Go очень чувствительный к этому.

Неправильно:

package main

//go:build linux

Правильно:

//go:build linux

package main

Build tags не работают внутри func. Только на уровне файла.

go test тоже поддерживает build tags. Хотите запустить тесты только на Linux?

go test -tags linux

В файле можно указать только ОДНУ строку //go:build.

Прокачать навык разработки на Golang можно на онлайн-курсе.

Автоматизация сборок с build tags

С ручным использованием go build ‑tags всё понятно, но что если нужно, чтобы в CI/CD сборки сами подхватывали нужные теги?

Допустим, есть Go‑сервис, который в проде работает с MySQL, а в тестах — с SQLite. Конечно, можно тащить оба драйвера в один бинарник и переключаться через конфиги, но это же Go, тут хочется быстрее.

Разделим код:

db_mysql.go

//go:build mysql

package db

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func Connect() (*sql.DB, error) {
    return sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
}

db_sqlite.go

//go:build sqlite

package db

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
)

func Connect() (*sql.DB, error) {
    return sql.Open("sqlite3", "file::memory:?cache=shared")
}

Теперь в CI/CD можно запускать разные билды:

# Сборка для продакшена (MySQL)
go build -tags mysql -o app-mysql

# Сборка для тестов (SQLite)
go build -tags sqlite -o app-sqlite

Не тянем лишние зависимости в бинарник, не переключаем драйверы в рантайме — просто компилируем разные версии.

Оптимизация производительности через build tags

Иногда мы пишем код, который в проде должен быть максимально быстрым, а в деве или тестах — наоборот, более отладочным.

Например, приложение должно логировать только ошибки, а в деве — всё подряд.

logger_prod.go

//go:build !debug

package logger

import "log"

func Log(msg string) {
    log.Println("[ERROR]:", msg)
}

logger_debug.go

//go:build debug

package logger

import "log"

func Log(msg string) {
    log.Println("[DEBUG]:", msg)
}

Теперь можно запускать билд с ‑tags debug, а прод‑серверы будут использовать дефолтный логгер.

# Разработчик билдит локально
go build -tags debug

# Продакшен-сервер билдит по умолчанию (без дебага)
go build

build tags и go:embed

Go 1.16 добавил //go:embed, позволяющий встраивать файлы в бинарник. Это удобно, но иногда нужно использовать разные ресурсы в разных окружениях.

Допустим, есть dev‑сборка, где фронтенд хостится отдельно, и prod‑сборка, где фронтенд зашит в Go‑бинарник.

assets_dev.go

//go:build !embed

package assets

import "errors"

func GetStaticFile(path string) ([]byte, error) {
    return nil, errors.New("static files are not embedded in dev mode")
}

assets_prod.go

//go:build embed

package assets

import (
    "embed"
    "io/fs"
)

//go:embed static/*
var embeddedFiles embed.FS

func GetStaticFile(path string) ([]byte, error) {
    return fs.ReadFile(embeddedFiles, "static/"+path)
}

Теперь можно собрать прод‑билд с фронтом:

go build -tags embed -o app-prod

А дев‑сборка останется лёгкой.


А как вы используете build tags в своих проектах? Делитесь опытом и фишками в комментариях.

11 марта пройдет открытый урок на тему «Дженерики в Gо». После урока вы сможете уверенно читать чужой код, в котором используются Дженерики, а также научитесь уместно их использовать в своём коде. Записаться можно по ссылке.

Полный список открытых уроков по разработке смотрите в календаре мероприятий.