Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Middle ~20 мин 7 вопросов

Сборка и деплой Go: флаги, кросс-компиляция, Docker — вопросы с ответами

Go компилируется в один статический бинарник — и именно поэтому вопросы сборки и деплоя на собеседовании проверяют понимание всей цепочки: от флагов компилятора до устройства Docker-образа. Ниже — вопросы с реальных интервью и ответы с кодом. Практическая база: тесты перед сборкой, контекст для graceful shutdown в деплое.

Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».

1. Какие флаги go build используют в продакшене и зачем?

Минимальный продакшен-набор: go build -trimpath -ldflags="-s -w" -o app ./cmd/app. Расшифровка: -trimpath убирает локальные пути из бинарника (воспроизводимая сборка, без утечки структуры каталогов); -s -w убирают таблицу символов и DWARF-отладку — бинарник меньше на ~30%; -o задаёт имя. Версию вшивают через -ldflags "-X main.version=v1.2.3" на пакетной переменной. Отдельно -buildvcs=false, если git-метаданные не нужны. На собеседовании ждут понимания, что каждый флаг решает конкретную задачу, а не заученного набора.

go build -trimpath \
  -ldflags="-s -w -X main.version=${VERSION}" \
  -o app ./cmd/app

2. Как работает кросс-компиляция в Go? Что такое GOOS и GOARCH?

Кросс-компиляция — сборка под чужую платформу — делается двумя переменными окружения без тулчейна целевой платформы: GOOS (ОС: linux, windows, darwin) и GOARCH (архитектура: amd64, arm64). Пример: GOOS=linux GOARCH=arm64 go build — бинарник для ARM-сервера соберётся прямо на Mac. Ограничение — CGO: если пакет использует C-код, нужен тулчейн целевой платформы, поэтому для чистой кросс-компиляции ставят CGO_ENABLED=0 (Go-код становится полностью статическим). Проверить платформу бинарника — file app или go version -m app (заодно показывает модули и флаги сборки).

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o app-linux-arm64 ./cmd/app
go version -m app # модули, версия Go, настройки сборки

3. CGO_ENABLED=0 — что меняется и когда нельзя отключать CGO?

CGO_ENABLED=0 выключает вызовы C-кода: бинарник становится полностью статическим (запускается где угодно, включая scratch и alpine без libc), кросс-компиляция работает без тулчейнов. Нельзя отключать, если зависимости требуют CGO: драйвер SQLite (mattn/go-sqlite3) — чисто CGO-шный (для него есть чисто-Go альтернатива modernc.org/sqlite), сетевые библиотеки с netgo-префиксами DNS-резолвера, некоторые крипто- и графические пакеты. Вопрос-ловушка: резолвер DNS — со включённым CGO Go может использовать системный libc-резолвер, с CGO_ENABLED=0 — свой чисто-Go; на редких конфигурациях DNS (например, сложный nsswitch) поведение различается.

CGO_ENABLED=0 go build -o app # статический бинарник
# не работает с mattn/go-sqlite3 — нужен CGO или modernc.org/sqlite

4. Multi-stage Dockerfile для Go: зачем две стадии и что положить в финальный образ?

Стадия builder компилирует бинарник (с полным Go-тулчейном и кэшем модулей), финальная стадия копирует ТОЛЬКО бинарник в минимальный базовый образ. Зачем: финальный образ без компилятора, исходников и кэша — меньше (десятки МБ вместо гигабайта), меньше поверхность атаки, быстрее деплой. Типовой финальный базовый образ: gcr.io/distroless/static или scratch — для CGO_ENABLED=0 бинарника больше ничего и не нужно (даже libc не требуется); нужен CA-сертификат для исходящих HTTPS (copy ca-certificates из builder или пакет ca-certificates в alpine). Плюс non-root USER и чтение конфигов из env — вопросы безопасности в довесок.

FROM golang:1.24 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /app ./cmd/app

FROM gcr.io/distroless/static-debian12
COPY --from=builder /app /app
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
USER nonroot:nonroot
ENTRYPOINT ["/app"]

5. Почему бинарник Go большой и как его уменьшить?

В Go в бинарник статически вшивается рантайм: планировщик, GC, reflection, все зависимости. Hello World весит ~2 МБ — это норма, а не баг. Способы уменьшения по убыванию эффекта: (1) -ldflags "-s -w" — минус таблица символов и отладка (~30%); (2) убрать неиспользуемые зависимости — каждая тянет свои пакеты; (3) UPX-сжатие — спорно: образ меньше, но при запуске распаковка в память и возможные срабатывания антивирусов; (4) проверить reflection-тяжёлые пакеты (ORM, крупные SDK). Правильный ответ подтверждает понимание: статический рантайм — это осознанный размен «размер на диск» на «работает везде без зависимостей».

6. Что должно быть в Docker-образе Go-сервиса, кроме бинарника?

Чек-лист для продакшен-образа: (1) CA-сертификаты — без них исходящий HTTPS упадёт с x509-ошибкой (самая частая ошибка первого Dockerfile); (2) часовой пояс — tzdata, если сервис формирует время в зонах (иначе всё в UTC); (3) non-root USER — безопасность; (4) HEALTHCHECK или readiness/liveness-эндпоинты для оркестратора; (5) правильные EXPOSE и обработку SIGTERM (graceful shutdown по сигналу — связка с контекстом). Пароли и секреты — только через env/secrets, не в слоях образа (слои видны history). Каждый пункт — типовой вопрос на собеседовании и типовая ошибка первого деплоя.

# Частая ошибка первого Dockerfile:
# нет ca-certificates → https://api.example.com падает с x509
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 65532:65532
ENTRYPOINT ["/app"]

7. Как встроить версию и метаданные сборки в бинарник?

Через -ldflags -X: она присваивает значение пакетной строковой переменной на этапе линковки: -X main.version=v1.2.3 -X main.commit=$(git rev-parse --short HEAD). Переменная должна быть пакетной (не локальной), типа string, без начального значения в коде (или с ним — перезапишется). Показывать — по флагу version или в /healthz-эндпоинте: сервис, который не может ответить «какая у меня версия», невозможно отлаживать в проде. Развитие вопроса: go version -m app показывает и модули, и build-настройки — удобно для разбора чужого бинарника.

// main.go
var version = "dev"
func main() {
    if *showVersion { fmt.Println(version); return }
    ...
}
// сборка:
go build -ldflags "-X main.version=$(git describe --tags)" -o app ./cmd/app

Прокачать тему в самоучителе

Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице: