Бинарь на 8 МБ в Go: как ужать до нужного размера
Пустой HTTP-сервис на Go компилируется в 8-мегабайтный бинарь ещё до того, как вы написали бизнес-логику. Разбираем, из чего он состоит и какими флагами и приёмами его реально уменьшить.

Собираете сервис на Go, смотрите на артефакт — 8 МБ. Кода в нём на три HTTP-хендлера и подключение к базе. В Docker-образ это тянет за собой лишний слой, в embedded-сборку не влезает, а в CI-артефакты копится десятками мегабайт на каждый билд. Разберём, откуда в бинаре эти мегабайты и как их убрать флагами линкера, режимом сборки и упаковкой — с замерами, чтобы понимать цену каждого шага.
Материал ориентирован на Go 1.22–1.23. Поведение флагов линкера и -trimpath в этих версиях стабильно; там, где что-то менялось, укажу отдельно.
Из чего складывается размер
Go компилирует статически: в бинарь попадает не только ваш код, но и рантайм, сборщик мусора, планировщик горутин и вся таблица типов для рефлексии. Даже package main с одним fmt.Println уже тянет часть рантайма и форматирование строк.
Крупные составляющие типичного бинаря:
- Рантайм Go — планировщик, GC, аллокатор. Это база, её не выкинуть.
- Отладочная информация — DWARF-секции и таблица символов. Нужны для
gdb,delveи стектрейсов с именами. В проде обычно не нужны. - Данные для рефлексии — имена типов, методов, полей структур. Растут вместе с числом типов, особенно если тянете тяжёлые библиотеки вроде драйверов БД или gRPC.
- Ваш код и зависимости — то, ради чего всё затевалось.
Посмотреть, кто занимает место, помогает пакет go tool nm и внешние инструменты вроде go-size-analyzer. Начните с замера, а не с флагов вслепую.
Флаги линкера: первое и самое дешёвое
Основной рычаг — флаг -ldflags с параметрами линкера -s и -w:
# Go 1.22+, сборка без отладочной информации
go build -ldflags="-s -w" -o app ./cmd/appЧто делает каждый:
-w— выкидывает DWARF-секции (информацию для отладчика).-s— выкидывает таблицу символов и информацию о номерах строк.
После этого delve и gdb по бинарю работать не будут, а в стектрейсах пропадут имена функций и файлов. Для прода это чаще приемлемо: паники всё равно ловятся логами и трейсингом, а не постмортем-отладчиком по коредампу.
Флаги -s -w — почти бесплатный выигрыш в 20–30% размера. Начинайте всегда с них, а всё остальное — уже по необходимости.Добавьте -trimpath, чтобы убрать из бинаря абсолютные пути сборочной машины. На размер он влияет слабо, но делает сборку воспроизводимой и не светит структуру каталогов CI:
go build -trimpath -ldflags="-s -w" -o app ./cmd/appCGO: выключить, если не нужен
Если код не использует C-библиотеки, отключите CGO. Это уберёт зависимость от системного libc, даст полностью статический бинарь и часто уменьшит размер:
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o app ./cmd/appПроверка результата — команда file app должна показать statically linked. Осторожно: некоторые пакеты (например, стандартный net с системным резолвером или SQLite-драйверы на C) в режиме CGO_ENABLED=0 ведут себя иначе или не собираются. Для net Go подставит чистый Go-резолвер — это меняет поведение DNS в некоторых конфигурациях.
Что даёт каждый шаг: замеры
Цифры ниже — порядок величины для небольшого HTTP-сервиса на net/http с драйвером PostgreSQL, собранного под Linux amd64 на Go 1.23. Абсолютные значения у вас будут другими, важны пропорции. Меряйте на своём коде: go build и ls -lh app.
| Сборка | Размер | Отладка / стектрейсы |
|---|---|---|
| go build (по умолчанию) | ~14 МБ | Полные |
| -ldflags="-s -w" | ~10 МБ | Без delve, имена в трейсах пропадают |
| + CGO_ENABLED=0 | ~10 МБ | То же, но статический |
| + upx --best | ~3–4 МБ | То же + медленный старт |
Основной прыжок дают флаги линкера. Дальше начинается зона компромиссов.
Упаковка UPX: последний рывок с ценой
UPX — упаковщик исполняемых файлов. Он сжимает бинарь, а при запуске тот распаковывает себя в память. Установите UPX отдельно (это внешняя утилита, не часть Go) и примените к уже собранному файлу:
# upx устанавливается отдельно, версия 4.x
upx --best --lzma appВыигрыш ощутимый — файл может ужаться в два-три раза. Но цена реальная:
- Медленнее старт. Распаковка в память добавляет задержку при каждом запуске. Для долгоживущего сервиса это разово, для CLI-утилиты, которую дёргают часто, — заметно.
- Больше памяти на старте. Распакованный образ живёт в RAM целиком.
- Ложные срабатывания антивирусов. Упакованные UPX бинари регулярно триггерят эвристику — это известная проблема для распространяемых утилит под Windows.
- Не работает поверх scratch без нюансов. В некоторых окружениях распаковка требует прав на исполнение изменяемой памяти.
Вывод: UPX уместен для утилит, которые вы раздаёте пользователям и где важен размер загрузки. Для серверного сервиса в Kubernetes лишние секунды старта и риск с антивирусами обычно не стоят сэкономленных мегабайт.
Docker: размер образа против размера бинаря
Часто настоящая цель — не бинарь сам по себе, а тонкий образ. Здесь работает multi-stage сборка: компилируете в полном образе, а в финальный кладёте только бинарь поверх scratch или distroless.
# builder
FROM golang:1.23 AS build
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 scratch
COPY --from=build /app /app
ENTRYPOINT ["/app"]Для scratch помните: нет ни /etc/ssl/certs, ни временной зоны, ни shell. Если сервис ходит по HTTPS, скопируйте корневые сертификаты отдельным COPY из builder-стадии (/etc/ssl/certs/ca-certificates.crt). Если нужны отладочные утилиты внутри контейнера — берите gcr.io/distroless/static вместо голого scratch.
Чего делать не стоит
Пара приёмов из советов в интернете, которые чаще вредят:
- Ручное вырезание пакетов из стандартной библиотеки. Линкер и так выкидывает неиспользуемый код (dead code elimination). Импорт
fmt, который вы "забыли", размер почти не двигает, если функции не вызываются. - Погоня за размером ценой рефлексии. Иногда советуют избегать библиотек, использующих рефлексию, ради размера. Если рефлексия нужна для дела (сериализация, ORM) — жертва размера того не стоит, экономия единицы процентов.
Что делать дальше
Порядок действий по убыванию отдачи и возрастанию цены: сначала -ldflags="-s -w" и CGO_ENABLED=0 — это безопасно и даёт основной эффект. Затем multi-stage Docker поверх distroless — если цель тонкий образ. UPX — только для раздаваемых утилит и только после замера, стоит ли выигрыш медленного старта.
Перед оптимизацией и после каждого шага замеряйте: ls -lh для размера, go tool nm -size app | sort и go-size-analyzer — чтобы увидеть, какая зависимость раздувает бинарь именно у вас. Часто выясняется, что 4 МБ приносит одна библиотека, и правильнее пересмотреть её, чем выжимать проценты флагами.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Command go — Build flags (официальная документация Go), Command link — флаги линкера (официальная документация Go)
Частые вопросы
У меня другая версия Go, флаги те же?
После -s -w пропали имена функций в панике, это нормально?
UPX безопасен для продакшена?
Почему даже пустая программа весит несколько мегабайт?
Не собирается с CGO_ENABLED=0, что делать?
Влияет ли уменьшение бинаря на производительность работающего сервиса?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.