Как управлять сборками в Go с помощью build tags
- четверг, 6 марта 2025 г. в 00:00:13
Привет, Хабр!
Вы когда‑нибудь сталкивались с ситуацией, когда нужно собрать Go‑приложение под несколько платформ? Или выключить часть кода в проде, оставив её активной в дев‑среде? Возможно, вы просто хотите поддерживать разные версии сборки с кастомными фичами без тонны if runtime.GOOS == «windows» {}
?
В этом вам помогут build tags.
Build tags в Go — это специальные комментарии, которые говорят компилятору:
«Этот файл включаем в сборку только если выполнены вот эти условия».
Вот как они выглядят:
//go:build linux
или (по старинке, но так писать уже не стоит):
// +build linux
Допустим, есть приложение, которое должно работать на 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
Этот файл просто не скомпилируется, пока мы сами не скажем обратное.
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 можно на онлайн-курсе.
С ручным использованием 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
Не тянем лишние зависимости в бинарник, не переключаем драйверы в рантайме — просто компилируем разные версии.
Иногда мы пишем код, который в проде должен быть максимально быстрым, а в деве или тестах — наоборот, более отладочным.
Например, приложение должно логировать только ошибки, а в деве — всё подряд.
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
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о». После урока вы сможете уверенно читать чужой код, в котором используются Дженерики, а также научитесь уместно их использовать в своём коде. Записаться можно по ссылке.
Полный список открытых уроков по разработке смотрите в календаре мероприятий.