Компиляция и запуск
В предыдущем уроке мы разобрали, из каких деталей состоит Go-программа. Вы уже десяток раз набирали go run main.go и видели результат. Но что стоит за этой командой? Куда девается скомпилированный файл? И как превратить код в бинарник — исполняемый файл с машинными инструкциями, который можно просто скопировать на сервер и запустить — без Go, без зависимостей, без ничего?
В первом уроке мы упоминали “компилируемый язык” и “один бинарник без зависимостей” — пришло время разобраться, что за этими словами стоит на практике. Компиляция — это перевод вашего кода в язык процессора: нули и единицы, которые CPU выполняет напрямую. Результат этого перевода — тот самый бинарник (от слова binary — двоичный). В отличие от Python, где код сначала компилируется в байткод, а затем выполняется виртуальной машиной (CPython VM), Go компилирует сразу в машинные инструкции процессора — без промежуточных слоёв. Отсюда и скорость.
Сегодня снимаем капот.
Для всех примеров в этом уроке будем использовать знакомый main.go:
package main
import "fmt"
func main() { fmt.Println("Привет, мир!")}Откройте терминал в VS Code (Ctrl+`) и убедитесь, что вы в папке с этим файлом.
go run — иллюзия интерпретатора
Если вы пришли из Python или JavaScript, go run ощущается привычно: набрал команду — программа заработала. Никаких промежуточных файлов, никакой возни. Кажется, что Go просто выполняет текст.
Но Go — компилируемый язык. Всегда. Даже когда go run прикидывается интерпретатором, под капотом разворачивается полноценная сборка:
- Создаётся временная директория (путь зависит от ОС)
- Исходный код компилируется в нативный бинарник
- Бинарник запускается как отдельный процесс
- После завершения программы временная директория удаляется
Именно поэтому в рабочей папке ничего не появляется — всё живёт и умирает во временном каталоге.
Заглянуть под капот
Хотите увидеть, что происходит? Добавьте флаг -x:
go run -x main.goВ терминале посыплются десятки строк — каждая команда, которую Go выполняет за кулисами. Там будет и путь к временной директории (WORK=...), и вызовы компилятора, и линковка.
А если хотите сохранить временные файлы для изучения — есть флаг -work:
go run -work main.goWORK=/var/folders/.../go-build3712456890 # путь зависит от ОСПривет, мир!Директория не удалится после завершения. Можете зайти туда и найти настоящий скомпилированный бинарник.
:::tip Лайфхак
go run -work — отличный способ убедиться, что Go действительно создаёт полноценный исполняемый файл, а не “интерпретирует” ваш код.
:::
Передача аргументов
В прошлом уроке мы писали программу greeter с os.Args. Запуск выглядел так:
go run main.go ВасяПривет, Вася!Всё что после имени файла — аргументы вашей программы. Go сам разберётся, где заканчиваются его флаги и начинаются ваши данные. Флаги Go идут до файла, аргументы программы — после:
go run -race main.go --port 8080# ^^^^ ^^^^^^^^^^# флаг Go аргументы вашей программыКстати, вместо go run main.go можно писать go run . — точка означает “весь пакет в текущей директории”. Пока у вас один файл — разницы нет, но это более идиоматичный* подход, и позже вы поймёте почему.
:::caution Точка работает только с Go-модулем
Команды с . (go run ., go build .) требуют файла go.mod в директории вашего проекта. Если его нет, Go выдаст ошибку. Создайте модуль одной командой:
go mod init hellohello — это имя вашего модуля (можно любое). После этого в директории появится файл go.mod. Подробнее про модули поговорим в отдельном уроке, а пока достаточно знать: перед первым использованием . выполните go mod init.
:::
* Идиоматичный — значит “так принято в сообществе”. У каждого языка есть негласные правила: не просто “работает”, а “так пишут опытные разработчики”. В Go-мире go run . — идиоматично, go run main.go — нет. Вы будете часто встречать это слово в Go-документации и статьях.
Ограничения
С версии Go 1.24 у go run появилось кэширование: если код не менялся, повторный запуск берёт готовый бинарник из кэша. Но ограничений всё равно хватает:
- Медленнее прямого запуска. Даже с кэшем
go runпроверяет актуальность сборки при каждом вызове. Для крупных проектов накладные расходы могут быть значительными — в некоторых случаях до 8 раз медленнее, чем запуск готового бинарника. - Нет контроля над бинарником. Он лежит где-то в кэше, вы не можете его передать или отправить на сервер.
os.Executable()вернёт путь во временную директорию — если ваша программа полагается на свой путь, это сломается. - Кэш недолговечен. Go агрессивно чистит кэшированные бинарники — примерно через 2 дня неиспользования. Утром
go runможет снова компилировать с нуля. - Только
package main. Нельзя запустить библиотечный пакет — только исполняемые программы. - Кросс-компиляция бессмысленна.
GOOS=linux go run .не имеет смысла — бинарник запускается на вашей машине, а не на целевой платформе. - Не для продакшена. Продакшен (production) — это среда, где ваша программа работает “по-настоящему”: обслуживает реальных пользователей, крутится на сервере 24/7. В противоположность — среда разработки (development), где вы пишете и тестируете код на своём компьютере.
go run— инструмент разработки, не способ деплоя в продакшен.
Вывод: go run — это “быстро глянуть”. Для всего остального есть go build.
go build — создаём настоящий бинарник
Когда нужен файл, который можно отправить коллеге, загрузить на сервер или положить в Docker-контейнер (Docker — система для упаковки и запуска приложений в изолированных средах, подробнее познакомимся позже) — используйте go build.
go build -o myapp ../myappПривет, мир!В директории появился файл myapp (на Windows — myapp.exe) размером около 2 МБ или чуть больше. Это самодостаточный бинарник. На целевой машине не нужен Go, не нужны библиотеки, не нужен рантайм. Просто копируете файл и запускаете.
Имя выходного файла
Без флага -o Go сам выберет имя:
go build . # Имя из go.mod (или имя директории)go build -o server . # Явно задаём имяgo build -o bin/app . # Можно указать и путьНа Windows автоматически добавится .exe.
:::tip go build без package main
Если запустить go build . в директории с библиотечным пакетом (не main), Go скомпилирует код и проверит его на ошибки, но не создаст выходного файла. Удобный способ валидировать код без засорения директории — особенно в CI.
:::
Флаг -race — детектор гонок данных
Представьте: два человека одновременно редактируют один документ, не видя друг друга. Один пишет заголовок, другой его удаляет — результат непредсказуем. В программировании это называется гонка данных (data race) — когда несколько частей программы одновременно читают и изменяют одну и ту же переменную. Результат зависит от того, кто успел первым, и каждый запуск может давать разный результат.
Go умеет такие ситуации находить автоматически. Вот пример — не пытайтесь пока разобрать каждую строку, ключевое слово go мы изучим в уроке про конкурентность (Concurrency). Сейчас важен сам принцип:
package main
import "fmt"
func main() { count := 0 for i := 0; i < 1000; i++ { go func() { // запускаем 1000 параллельных задач count++ // все пишут в одну переменную }() } fmt.Println(count)}Запустим без флага — программа молча выдаст непредсказуемый результат:
go run .0 # или 127, или 999 — каждый раз по-разномуТеперь с -race:
go run -race main.go==================WARNING: DATA RACERead at 0x00c00011c028 by goroutine 9: main.main.func1() /home/user/main.go:9 +0x2e
Previous write at 0x00c00011c028 by goroutine 8: main.main.func1() /home/user/main.go:9 +0x44
Goroutine 9 (running) created at: main.main() /home/user/main.go:8 +0x4a
Goroutine 8 (finished) created at: main.main() /home/user/main.go:8 +0x4a==================... ещё 2 похожих предупреждения ...651Found 3 data race(s)exit status 66Go нашёл 3 гонки и точно показывает: строка 9, переменная count (адрес 0x00c00011c028), несколько параллельных задач пишут в неё одновременно. Программа завершилась с кодом 66 — специальный код ошибки для гонок. Число 651 вместо ожидаемых 1000 — результат потерянных обновлений, типичное последствие гонки данных. Без -race программа бы тихо выдавала неправильные результаты — баг, который крайне сложно поймать вручную.
Флаг -race работает и с go build, и с go run, и с go test. Бинарник с ним медленнее и потребляет больше памяти, поэтому в продакшен его не ставят. Но в тестах и при разработке — обязательная практика.
Почему Hello World весит 2 мегабайта?
После первого go build новички удивляются: программа из пяти строк — и 2 МБ? На C аналог весит 16 КБ.
Дело в том, что Go-бинарник — это не просто ваш код. Это целая вселенная:
- Go runtime — среда выполнения
- Garbage Collector — сборщик мусора
- Goroutine Scheduler — планировщик горутин (помните из первого урока — многозадачность из коробки?)
- Таблица символов и отладочная информация — для паник-трейсов и отладки
- Части стандартной библиотеки, которые вы импортировали
Один только import "fmt" тянет за собой рефлексию, I/O и форматирование строк*. Всё встроено внутрь.
- * Рефлексия (Reflection) — механизм, позволяющий программе анализировать и изменять собственную структуру во время выполнения: узнавать типы переменных, читать их значения, вызывать функции по имени. Проще говоря — способность программы “смотреть на саму себя”. Вспомните: вы пишете
fmt.Println(42)— и получаете42. Пишетеfmt.Println("привет")— получаетепривет. Пишетеfmt.Println(3.14)— получаете3.14. ОткудаPrintlnзнает, как напечатать каждое из этих значений, если число и строка — совершенно разные вещи? Именно через рефлексию: в момент вызова функция спрашивает у рантайма “что мне передали — число? строку? что-то ещё?” — и на основе ответа выбирает, как это отобразить. Без рефлексии пришлось бы писать отдельную функцию для каждого типа данных. - * I/O (Input/Output, ввод/вывод) — всё что связано с чтением и записью: вывод в терминал, чтение файлов, передача данных по сети.
fmt.Printlnзаписывает текст в стандартный поток вывода (stdout) — это I/O. - * Форматирование строк — превращение данных в читаемый текст. Когда вы пишете
fmt.Println("Ответ:", 42), Go преобразует число42в строку"42"и склеивает с"Ответ:".
Именно поэтому в первом уроке мы говорили “один бинарник без зависимостей” — теперь вы понимаете, что это значит. Go кладёт всё необходимое прямо в файл.
Уменьшаем размер
Зачем уменьшать размер? Меньший бинарник — это быстрее скачивание на сервер (особенно если серверов десятки), меньше места в Docker-образе, быстрее запуск контейнеров. Для Hello World разница незаметна, но когда проект вырастет до 20–50 МБ — экономия 25–30% уже ощутима.
# Стандартная сборкаgo build -o app .# app — ~2 МБ
# Без отладочной информации (-s: символы, -w: DWARF)go build -ldflags="-s -w" -o app .# app — ~1.5 МБФлаги -s -w убирают таблицу символов и отладочную информацию DWARF. На практике это минус 25–30% от размера. Паник-трейсы при этом продолжают работать — Go хранит свою внутреннюю таблицу для отслеживания ошибок отдельно.
:::tip Рецепт для продакшена
go build -ldflags="-s -w" -trimpath -o app .Флаг -trimpath дополнительно вырезает из бинарника абсолютные пути вашей файловой системы. Бонус к безопасности и воспроизводимости сборки.
:::
Встраиваем версию
Представьте: вы отправили бинарник на сервер. Через месяц что-то сломалось. Какая версия кода там стоит — вы уже не помните. Пересобирали с тех пор десять раз. Если бинарник сам умеет ответить “я версия 1.0.0” — проблема решается за секунду: запустили с флагом --version, сравнили с актуальной, поняли нужно ли обновлять.
Именно для этого версию вшивают прямо в бинарник на этапе сборки:
package main
import "fmt"
var version = "dev"
func main() { fmt.Println("Версия:", version)}В коде version равна "dev" — значение по умолчанию для разработки. Но при сборке мы подменяем её:
go build -ldflags="-X main.version=1.0.0" -o app ../appВерсия: 1.0.0Флаг -X подменяет значение строковой переменной на этапе компиляции — в самом исходном коде ничего менять не надо. В автоматизированных системах сборки — CI/CD (Continuous Integration / Continuous Delivery, автоматическая сборка и доставка кода) — это стандартная практика: вшивают номер версии, дату сборки, идентификатор изменения из Git. Любой бинарник может “представиться”.
:::tip Начиная с Go 1.24
Go автоматически вшивает информацию из Git (идентификатор изменения, тег версии, пометку +dirty если есть несохранённые правки). Посмотреть можно через go version -m ./app.
:::
Build cache — почему второй раз быстрее
Кэш (cache) — место, где хранятся результаты предыдущей работы, чтобы не делать её заново. Браузер кэширует картинки с сайтов, чтобы не скачивать их повторно. Go делает то же самое с компиляцией: сохраняет уже скомпилированные пакеты, а при следующей сборке компилирует только те, что изменились.
go build -o app . # Первый раз: ~2 секундыgo build -o app . # Второй раз: ~0.3 секунды (линковка)Где именно Go хранит кэш на вашем компьютере, можно узнать командой:
go env GOCACHEПуть зависит от ОС — на Linux, macOS и Windows он будет разным. Если что-то пошло не так и хотите очистить кэш — go clean -cache.
:::tip Для опытных: PGO — оптимизация по профилю
Начиная с Go 1.22, Profile-Guided Optimization стабильна. Идея: вы снимаете CPU-профиль работающего приложения (через pprof), кладёте файл default.pgo в корень проекта, и при следующей сборке компилятор использует реальные данные о горячих путях для агрессивного инлайнинга и оптимизации. Прирост производительности — 2–14% без единого изменения в коде. Подробнее — в уроках про тестирование (Testing) и профилирование (Profiling).
:::
go install — ставим утилиты глобально
go build создаёт бинарник в текущей папке — там, где вы сейчас стоите в терминале. Это удобно для вашего проекта, но что если вы хотите поставить чужую утилиту — линтер, форматтер, генератор кода — и пользоваться ей из любого места в системе?
Для этого есть go install. Он делает то же, что go build, но кладёт готовый бинарник не рядом с вами, а в специальную папку. В уроке про установку мы видели переменную GOBIN в таблице — вот это она и есть. Узнать, куда именно Go кладёт утилиты на вашей системе, можно командой:
go env GOPATHБинарники попадут в подпапку bin этого пути. Если вы при установке Go добавили эту папку в PATH — утилиты будут доступны из любой директории. Если нет — самое время это сделать (подробнее в уроке про установку).
Пример — установим goimports, утилиту для автоматической расстановки импортов:
go install golang.org/x/tools/cmd/goimports@latestЭта команда скачивает исходный код goimports с golang.org/x/tools, компилирует его и кладёт готовый бинарник в ту самую папку bin. Суффикс @latest означает “последняя версия”. Теперь утилита доступна глобально:
goimports -w main.goЭто работает из любой папки — при условии, что папка bin добавлена в PATH. Именно так устанавливались инструменты в уроке про редактор — gopls, dlv и другие расширения VS Code ставились через go install.
Отличие от go build
go build | go install | |
|---|---|---|
| Куда кладёт бинарник | В текущую папку | В папку bin внутри GOPATH |
| Для чего | Ваш проект | Чужие утилиты |
| Доступен глобально | Нет | Да (если папка bin в PATH) |
| Типичное использование | go build -o server . | go install tool@latest |
:::tip Для опытных: директива tool в go.mod (Go 1.24)
Раньше, чтобы зафиксировать версию утилиты (линтера, кодогенератора) на весь проект, использовали хак — файл tools.go с blank import-ами. С Go 1.24 появилась директива tool прямо в go.mod:
go get -tool github.com/golangci/golangci-lint/cmd/golangci-lint@latestЭто добавит tool строку в go.mod, и теперь любой участник команды запускает утилиту через go tool golangci-lint run ./... — гарантированно та же версия, что и у всех. Никаких расхождений между разработчиками и CI.
:::
Когда что использовать
go run | go build | go install | |
|---|---|---|---|
| Что делает | Компилирует → запускает → удаляет | Компилирует → сохраняет | Компилирует → кладёт в GOPATH/bin |
| Файл остаётся | ❌ Нет | ✅ В текущей папке | ✅ В GOPATH/bin |
| Когда использовать | Разработка, эксперименты | Сборка для сервера, Docker | Установка CLI-утилит |
| Кэширование | С Go 1.24 | Промежуточные файлы | Промежуточные файлы |
Типичный рабочий процесс:
go run .— пока пишете и отлаживаетеgo build— когда нужен финальный артефактgo install— для утилит, которыми пользуетесь постоянно
Кросс-компиляция — собираем под любую ОС
Важный момент: каждый бинарник собирается под конкретную ОС и архитектуру. Бинарник, собранный на macOS, не запустится на Linux — получите ошибку Exec format error. И наоборот. Это не как Python-скрипт, который одинаково работает везде, где стоит интерпретатор. Бинарник содержит машинные инструкции для конкретной платформы.
Если вы разрабатываете на macOS, а сервер работает на Linux — нужно собрать бинарник именно для Linux. Это и есть кросс-компиляция: сборка на одной платформе для другой. В первом уроке мы упоминали “собрал под Linux на маке одной командой” — пришло время показать, как.
Помните код из урока про установку?
fmt.Printf("OS: %s\n", runtime.GOOS)fmt.Printf("Arch: %s\n", runtime.GOARCH)Тогда runtime.GOOS показывал вашу текущую ОС — и это логично, ведь вы собирали и запускали на одной машине. Но runtime.GOOS и runtime.GOARCH — это не “определение системы в момент запуска”. Это константы, зашитые в бинарник на этапе компиляции. По умолчанию Go ставит туда вашу ОС и архитектуру, поэтому всё совпадает. Но их можно подменить двумя переменными окружения — и тогда бинарник будет собран для другой платформы:
# Собираем на Mac, запускаем на Linux-сервереGOOS=linux GOARCH=amd64 go build -o app-linux .
# Для WindowsGOOS=windows GOARCH=amd64 go build -o app.exe .
# Для Mac с Apple SiliconGOOS=darwin GOARCH=arm64 go build -o app-mac .
# Для Raspberry PiGOOS=linux GOARCH=arm GOARM=7 go build -o app-rpi .Никаких дополнительных компиляторов, тулчейнов, виртуальных машин. Go всё умеет из коробки.
Хотите увидеть все поддерживаемые платформы? Их больше 45:
go tool dist listaix/ppc64android/amd64darwin/amd64darwin/arm64js/wasmlinux/amd64linux/arm64wasip1/wasmwindows/amd64... и ещё 40+CGO_ENABLED=0 — полностью статический бинарник
Мы говорили, что Go-бинарник самодостаточный — внутри всё необходимое. Но это не совсем так. По умолчанию Go в некоторых случаях использует C-библиотеки операционной системы — например, для работы с DNS (преобразование доменных имён в IP-адреса) и для получения информации о пользователях системы. Это называется CGO (C-Go) — мост между Go и кодом на языке C.
Проблема в том, что C-библиотеки разные на разных системах. Бинарник, собранный с CGO, ожидает определённую C-библиотеку на целевой системе. Если её там нет или стоит другая версия — бинарник упадёт с непонятной ошибкой.
Чтобы бинарник был по-настоящему самодостаточным и не зависел ни от чего на целевой системе, отключаем CGO:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .CGO_ENABLED=0 говорит Go: “не используй C-библиотеки, сделай всё сам”. Go заменит системные вызовы своими реализациями на чистом Go. Результат — бинарник, которому нужна только операционная система. Никаких библиотек, никаких зависимостей.
:::danger Это важно для Docker
Популярный базовый образ Alpine Linux использует musl libc вместо стандартной glibc. Если собрать бинарник без CGO_ENABLED=0 — в Alpine он упадёт с загадочной ошибкой no such file or directory, хотя файл на месте. Система не может найти C-библиотеку, которую бинарник ожидает. Правило простое: для Docker-сборок всегда ставьте CGO_ENABLED=0.
:::
Что под капотом: почему Go компилируется за секунды
В первом уроке мы говорили, что Go создали, потому что создатели устали ждать компиляцию C++ по 45 минут. Вот как они решили эту проблему.
Этапы компиляции (на пальцах)
Когда вы набираете go build, происходит следующее:

- Lexer (Scanner) — разбивает текст на токены: ключевые слова, имена, числа, операторы
- Parser — собирает токены в дерево (AST — Abstract Syntax Tree), отражающее структуру программы
- Semantic Analysis (Type Checker) — проверяет типы: “x — это int, нельзя сложить со строкой”
- Intermediate Representation — преобразует AST в промежуточное представление (IR)
- SSA (Static Single Assignment) — оптимизирует IR: удаляет мёртвый код, сворачивает константы
- Machine Code Generation — превращает SSA в инструкции для конкретного процессора
- Linker — склеивает машинный код с runtime → готовый бинарник
- Execution — ОС загружает бинарник и запускает
Вы не обязаны понимать каждый шаг. Важно знать, что компиляция — это перевод: человеческий текст → машинные инструкции. Go делает этот перевод очень быстро.
:::tip Для опытных: что происходит на каждом этапе
SSA (Static Single Assignment) — ключевое промежуточное представление. Каждой переменной присваивается значение ровно один раз: x = 1; x = x + 2 превращается в x₁ = 1; x₂ = x₁ + 2. Это упрощает оптимизации: удаление мёртвого кода, свёртку констант, устранение лишних проверок границ массивов.
Escape analysis — компилятор решает, где жить переменной: на стеке (быстро, бесплатная очистка) или в куче (дороже, нагружает GC — Garbage Collector, сборщик мусора, который автоматически освобождает неиспользуемую память). Если ссылка на переменную “утекает” за пределы функции — куча. Иначе — стек. Посмотреть решения компилятора: go build -gcflags="-m" .
Inlining — компилятор подставляет тело маленьких функций прямо в место вызова, убирая накладные расходы. Go инлайнит функции до определённой “стоимости” (80 узлов AST). С Go 1.22 инлайнер стал агрессивнее. :::
:::tip Для опытных: что происходит до main()
Когда ОС запускает Go-бинарник, до вашего main() далеко:
- OS Loader загружает файл в память, находит точку входа
- Ассемблерный bootstrap (
_rt0_amd64_linuxи подобные) сохраняет argc/argv runtime.rt0_goвыделяет стек для системной горутины g0, инициализирует кучу- Запуск подсистем — GC, планировщик горутин, сетевой поллер
init()функции всех импортированных пакетов — снизу вверх по дереву зависимостейmain.main()— наконец-то ваш код
Именно поэтому даже пустая Go-программа “тяжелее” аналога на C — она несёт с собой полноценную среду выполнения. :::
Почему быстрее C++ и Rust
Главная причина — модель зависимостей. Роб Пайк измерял: при компиляции Go-кода компилятор читает в 40 раз меньше исходного текста, чем при компиляции C++. В C++ каждый #include <string> заново “объясняет” компилятору, что такое строки. В Go информация о пакете хранится в скомпилированном виде — компилятор читает только прямые импорты, не проваливаясь в их зависимости.
Другие факторы:
- 25 ключевых слов — парсер работает мгновенно
- Запрет циклических импортов — граф зависимостей можно компилировать параллельно
- Неиспользуемый импорт = ошибка — компилятор не тратит время на мёртвый код
- Нет шаблонов как в C++ — не нужно раздувать код при инстанциации
Для масштаба: проект Istio (платформа для управления микросервисами, ~350 000 строк Go) компилируется с нуля за 33 секунды на мощной машине. С прогретым кэшем — меньше секунды.
:::tip Для любопытных
go build -x покажет каждую команду, которую выполняет тулчейн: компиляцию каждого пакета, генерацию конфигов, линковку. Десятки строк вывода — и всё это за пару секунд.
:::
Практические сценарии
Docker multi-stage build
Стандартный способ доставки Go-сервиса на сервер — многоэтапная сборка Docker:
# Этап 1: собираем бинарникFROM golang:1.25 AS builderWORKDIR /appCOPY go.mod go.sum ./ # go.sum — файл с контрольными суммами зависимостейRUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o server .
# Этап 2: минимальный образFROM scratchCOPY --from=builder /app/server /serverENTRYPOINT ["/server"]Образ scratch — абсолютно пустой. Ни bash, ни curl, ни libc. Только ваш бинарник. Финальный образ: 3–5 МБ вместо 700 МБ+ с полным SDK (Software Development Kit — набор инструментов для разработки). Это возможно именно потому, что Go-бинарник самодостаточный.
Makefile
Makefile — это файл с набором команд-рецептов. Вместо того чтобы каждый раз набирать длинную команду сборки с десятком флагов, вы описываете её один раз в Makefile и потом вызываете коротким make build. Утилита make есть на Linux и macOS из коробки, для Windows можно установить отдельно.
Для проектов побольше удобно завернуть команды в Makefile:
APP_NAME = myapp.PHONY: run build test clean cross
run: go run .build: go build -ldflags="-s -w" -trimpath -o $(APP_NAME) .test: go test -race ./...clean: rm -f $(APP_NAME) $(APP_NAME)-*cross: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o $(APP_NAME)-linux . CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o $(APP_NAME)-mac . CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o $(APP_NAME).exe .Теперь make build вместо длинной команды с флагами. make cross — сборка под три ОС разом.
:::tip Для опытных: GoReleaser
Когда проект дорастёт до публичных релизов — кросс-компиляция, архивы, changelog, публикация на GitHub/GitLab, конфиги для Homebrew и Scoop — вручную это ад. GoReleaser автоматизирует весь цикл. Один файл .goreleaser.yaml, одна команда — и релиз под десятки платформ готов. Его используют Kubernetes, Docker, GitHub CLI и тысячи open-source проектов.
:::
Типичные грабли новичков
1. “undefined” при go run main.go
Когда проект подрастёт, код естественно разобьётся на несколько файлов — например, main.go и math.go в одном пакете main. И тут есть нюанс.
Когда вы явно указываете файлы — go run main.go — Go считает только перечисленные файлы частью вашего пакета. Остальные .go-файлы в той же директории он не видит, как будто их нет:
go run main.go./main.go:10:2: undefined: Add # функция Add из math.go — "не существует"
go run . # ✅ все файлы пакета включеныВажно не путать две вещи:
- Соседние файлы того же пакета (например
math.goсpackage mainв той же папке) — приgo run main.goне подтягиваются. Это и есть проблема. - Импортированные пакеты (через
import) — подтягиваются нормально. Если у васimport "myproject/utils", пакетutilsскомпилируется, потому что он разрешается через модульную систему, а не через “соседние файлы в директории”.
go run . и go build . включают все .go-файлы в директории (кроме _test.go). Поэтому привыкайте к go run . — с ней таких проблем не будет.
2. “cannot run non-main package”
go run .go run: cannot run non-main packageЗабыли написать package main или нет функции main(). Помните из прошлого урока: только package main + func main() создают исполняемую программу.
3. Бинарник не запускается на другой ОС
Собрали на Mac, скопировали на Linux-сервер:
./myappbash: ./myapp: cannot execute binary file: Exec format errorЭто бинарник для macOS, а запускаете на Linux. Нужна кросс-компиляция:
GOOS=linux GOARCH=amd64 go build -o myapp .4. “permission denied” (Linux/macOS)
./myappbash: ./myapp: Permission deniedНет прав на выполнение. go build ставит их автоматически, но при копировании через архив или сеть бит выполнения может потеряться. На Windows такой проблемы нет — там работает по-другому.
chmod +x myapp5. Загадочная “no such file or directory” в Docker
exec /server: no such file or directoryФайл на месте, но система не может найти динамический загрузчик glibc. Собрали без CGO_ENABLED=0, а запускаете в Alpine с musl.
# Правильная сборка для Docker:CGO_ENABLED=0 go build -o server .Полный пример: от кода до бинарника
Возьмём программу greeter из прошлого урока и пройдём полный цикл:
package main
import ( "fmt" "os" "strings")
var version = "dev"
func main() { if len(os.Args) > 1 && os.Args[1] == "--version" { fmt.Println(version) return }
name := "Мир" if len(os.Args) > 1 { name = strings.Join(os.Args[1:], " ") } fmt.Printf("Привет, %s!\n", name)}# 1. Быстрый запуск во время разработкиgo run . ВасяПривет, Вася!
# 2. Собираем бинарник с версиейgo build -ldflags="-s -w -X main.version=1.0.0" -trimpath -o greeter .
# 3. Проверяем./greeter --version1.0.0./greeter дорогой другПривет, дорогой друг!
# 4. Кросс-компиляция для Linux-сервераCGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ go build -ldflags="-s -w -X main.version=1.0.0" -trimpath -o greeter-linux .
# 5. Оба файла — около 1.3 МБ каждыйОдин исходник — бинарники под любую платформу. Без Docker, без виртуальных машин, без боли.
Итоги
| Команда / Концепция | Что помнить |
|---|---|
go run . | Компилирует и запускает. Бинарник временный. Для разработки. |
go build -o app . | Создаёт постоянный бинарник. Для деплоя и CI/CD. |
go install | Ставит бинарник в GOPATH/bin. Для CLI-утилит. |
-ldflags="-s -w" | Убирает отладочную инфу. Минус 25–30% размера. |
-trimpath | Убирает пути. Безопасность + воспроизводимость. |
-X main.var=val | Вшивает значение переменной при сборке. |
GOOS / GOARCH | Кросс-компиляция. Любая ОС, любая архитектура. |
CGO_ENABLED=0 | Полностью статический бинарник. Обязателен для Docker. |
go build -x | Показывает все шаги компиляции. Для любопытных. |
Задачи
Задача 1: Заглядываем под капот ⭐
Запустите программу Hello World так, чтобы увидеть путь к временной директории сборки. Временные файлы не должны удаляться после запуска.
Решение
go run -work main.goWORK=/var/folders/.../go-build1234567890 # путь зависит от ОСПривет, мир!Флаг -work печатает путь и сохраняет временную директорию. Можно зайти в неё и найти скомпилированный бинарник.
Задача 2: Оптимальная сборка ⭐⭐
Соберите бинарник для Linux ARM64 с минимальным размером, без путей файловой системы и без зависимостей от C-библиотек. Имя файла — server.
Решение
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \ go build -ldflags="-s -w" -trimpath -o server .CGO_ENABLED=0— статический бинарник без C-зависимостейGOOS=linux GOARCH=arm64— целевая платформа-ldflags="-s -w"— убираем символы и DWARF-trimpath— убираем абсолютные пути
Задача 3: Версия из командной строки ⭐⭐⭐
Напишите программу, которая при запуске с флагом --version выводит номер версии, а без флага — приветствие. Версия должна вшиваться при сборке через -ldflags, а не хардкодиться.
go build -ldflags="-X main.version=2.1.0" -o app ../app --versionapp v2.1.0./appПривет из app!Подсказка
Объявите var version = "dev" и используйте os.Args для проверки аргументов.
Решение
package main
import ( "fmt" "os")
var version = "dev"
func main() { if len(os.Args) > 1 && os.Args[1] == "--version" { fmt.Println("app v" + version) return } fmt.Println("Привет из app!")}Сборка:
go build -ldflags="-X main.version=2.1.0" -o app .Задача 4: Мультиплатформенная сборка ⭐⭐⭐
Напишите скрипт (или команды), который соберёт один и тот же проект под три платформы: Linux amd64, macOS arm64, Windows amd64. Все бинарники должны быть статическими, минимального размера и лежать в папке dist/.
Решение
mkdir -p dist
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o dist/app-linux .CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o dist/app-mac .CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o dist/app.exe .Или через Makefile:
.PHONY: distdist: mkdir -p dist CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o dist/app-linux . CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o dist/app-mac . CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o dist/app.exe .Что дальше?
Теперь вы умеете превращать код в бинарник, собирать под любую платформу и понимаете, что происходит за кулисами компиляции. В следующем уроке разберём Go Playground — веб-среду для запуска Go-кода прямо в браузере: быстрые эксперименты, обмен сниппетами с коллегами и тестирование идей без локальной установки.
Источники
- Go Command Documentation — официальная документация команды
go - Go at Google: Language Design in the Service of Software Engineering — Роб Пайк о дизайне Go
- Go 1.24 Release Notes — кэширование
go run, директиваtool - Go 1.25 Release Notes — DWARF v5, автоматический GOMAXPROCS
- How to Reduce Go Binary Size — Filippo Valsorda об оптимизации размера
- Effective Go — рекомендации по идиоматичному Go
- Understanding Go Compiler — Kanishka Naik о пайплайне компиляции Go