33. Сборка под разные платформы
От теории к практике: Матрица сборки
В предыдущей статье мы исследовали архитектурную механику кросс-компилятора Go. Однако в реальном промышленном цикле разработки перед инженером встает практическая задача: получить из одного репозитория полный спектр бинарных артефактов для всех поддерживаемых платформ. Нам нужен серверный демон под Linux (x86-64 и ARM64), консольная утилита администрирования для инженеров на macOS (Apple Silicon и Intel) и клиентское приложение для пользователей Windows.
Организация параллельной матричной сборки требует продуманной дисциплины именования файлов, четкого управления платформо-зависимым кодом и применения современных инструментов контейнеризации.
Стратегия именования артефактов
Если при пакетной сборке вызвать команду go build -o app сначала для Linux, а затем для Windows, результирующие файлы либо перезапишут друг друга, либо приведут к коллизии имен в каталоге артефактов.
Общепринятый индустриальный стандарт требует включать имя программы, семантическую версию, целевую ОС и архитектуру процессора прямо в имя файла.
Формат: <app>-<version>-<os>-<arch>[.exe]
Пример классического сборочного bash-сценария:
VERSION=$(git describe --tags --always)
APP_NAME="myapp"
PLATFORMS=("linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64" "windows/amd64")
for PLATFORM in "${PLATFORMS[@]}"; do
IFS='/' read -r GOOS GOARCH <<< "$PLATFORM"
OUTPUT_NAME="${APP_NAME}-${VERSION}-${GOOS}-${GOARCH}"
if [ "$GOOS" = "windows" ]; then
OUTPUT_NAME+=".exe"
fi
echo "Building for $GOOS/$GOARCH..."
env GOOS="$GOOS" GOARCH="$GOARCH" go build -ldflags="-s -w" -o "bin/$OUTPUT_NAME" ./cmd/app
done
Этот подход часто оформляется в виде целей Makefile. Однако в современных производственных пайплайнах всю эту рутину обычно доверяют специализированному декларативному инструменту GoReleaser (см. 31. Release pipeline и versioning).
Платформо-зависимый код: Build Tags и суффиксы
Иногда поведение сервиса на разных операционных системах должно различаться фундаментально: например, обработка сигналов завершения syscall.SIGTERM в Unix против событий службы Windows Service, или низкоуровневая работа с реестром ОС.
В языке C для таких целей использовались директивы препроцессора #ifdef, превращавшие исходный код в нечитаемую «вермишель». Создатели Go предложили два гораздо более чистых и структурированных механизма.
1. Неявные суффиксы файлов (Implicit Suffixes)
Компилятор Go автоматически фильтрует файлы исходного кода на этапе построения AST, если их имена оканчиваются на _<GOOS>.go, _<GOARCH>.go или _<GOOS>_<GOARCH>.go:
sound.go // Общий интерфейс
sound_linux.go // Реализация через ALSA/PulseAudio (только для linux)
sound_windows.go // Реализация через WinAPI (только для windows)
sound_darwin.go // Реализация через CoreAudio (только для darwin)
Если задана целевая сборка GOOS=linux, файл sound_windows.go будет полностью проигнорирован компилятором еще до проверки типов, словно его вовсе не существует в каталоге.
2. Явные теги сборки (//go:build)
Когда условий включения файла несколько или требуется булева логика (дизъюнкция, конъюнкция, отрицание), применяются директивы Build Tags в первой строке файла.
Пример для файла server_linux_arm.go:
//go:build linux && arm
package server
// Этот код скомпилируется только если GOOS=linux И GOARCH=arm
Пример для файла log_unix.go:
//go:build darwin || linux || freebsd
package log
// Этот код скомпилируется для любой UNIX-подобной системы
flowchart TD
A["go build GOOS=linux"] --> B{"File Selection"}
B --> C["utils.go"]
B --> D["utils_linux.go"]
B --> E["utils_windows.go"]
D --> F["Included"]
E --> G["Ignored"]
F --> H["Compilation"]
style A fill:#4b5563,stroke:#9ca3af,color:#fff
style B fill:#1e293b,stroke:#38bdf8,color:#fff
style D fill:#1e293b,stroke:#10b981,color:#fff
style E fill:#1e293b,stroke:#ef4444,color:#fff
style F fill:#065f46,stroke:#34d399,color:#fff
style G fill:#991b1b,stroke:#f87171,color:#fff
style H fill:#1e40af,stroke:#60a5fa,color:#fff
Мультиплатформенные Docker-образы: docker buildx
Современные облачные кластеры часто строятся на гетерогенном железе: узлы с дешевыми и энергоэффективными процессорами ARM64 (AWS Graviton, Ampere Altra) работают бок о бок с серверами на базе x86-64 (Intel Xeon, AMD EPYC).
Реестры контейнеров (Docker Hub, GitHub Packages, GitLab Registry) поддерживают стандарт OCI Manifest List (список манифестов). Это позволяет опубликовать единый публичный тег (например, myapp:latest), под капотом которого скрываются разные бинарные образы для архитектур AMD64 и ARM64. Контейнерный движок (containerd или CRI-O) на сервере автоматически запросит именно тот срез, который соответствует микропроцессору хоста.
Для компиляции мультиплатформенных образов применяется плагин docker buildx:
# Создаем и используем билдер
docker buildx create --name mybuilder --use
# Собираем и пушим сразу под 2 архитектуры
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myuser/myapp:latest \
--push .
Два подхода к мультиплатформенной сборке в Docker
- Эмуляция процессора через QEMU:
Если агент сборки работает на AMD64, он может исполнять ARM64-контейнеры через системный транслятор инструкций QEMU (binfmt_misc). Это универсально, но крайне неэффективно: трансляция машинных инструкций на лету замедляет компиляцию в 10–50 раз. - Нативная кросс-компиляция Go в Dockerfile:
Сборочный движок BuildKit передает вDockerfileвстроенные аргументы сборкиTARGETOSиTARGETARCH. Мы можем выполнять компилятор Go на нативной скорости процессора хоста, указывая ему целевую архитектуру:
# Dockerfile для multi-platform
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /app ./cmd/app
В этом сценарии компилятор Go работает на аппаратной скорости хостового процессора, создавая машинный код целевой архитектуры за считанные секунды без малейших накладных расходов на эмуляцию.
Итог
- Используйте матричную сборку для создания артефактов под все платформы.
- Именуйте файлы по стандарту
app-os-arch. - Используйте суффиксы файлов (
_linux.go) для разделения кода. docker buildx— стандарт для создания мульти-архитектурных Docker-образов.
Мы научились организовывать матричную кросс-платформенную сборку. Однако этот стройный конвейер безупречно работает лишь до тех пор, пока в процесс сборки не вмешивается код на языке C.
В следующей статье мы исследуем механизм CGO и его фундаментальное влияние на время сборки, линковку и стабильность бинарников: 34. CGO и его влияние на сборку.