Привет, Хабр!
Вы когда‑нибудь сталкивались с ситуацией, когда нужно собрать 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о». После урока вы сможете уверенно читать чужой код, в котором используются Дженерики, а также научитесь уместно их использовать в своём коде. Записаться можно по ссылке.
Полный список открытых уроков по разработке смотрите в календаре мероприятий.
