Как скомпилировать go файл

Обновлено: 06.07.2024

build компилирует пакеты, названные путями импорта, вместе с их зависимостями, но не устанавливает результаты.

Если аргументы для build представляют собой список файлов .go, build обрабатывает их как список исходных файлов, указывающих один пакет.

При компиляции единичного основного пакета (main package) build записывает полученный исполняемый файл в выходной файл, названный в честь первого исходного файла ("go build ed.go rx.go" записывает "ed" или "ed.exe") или в каталог исходного кода ("go build unix/sam" записывает "sam" или "sam.exe"). Суффикс ".exe" добавляется при записи исполняемого файла Windows.

При компиляции нескольких пакетов или одного неосновного пакета, build компилирует пакеты, но отбрасывает полученный объект, служа только для проверки возможности сборки пакетов.

При компиляции пакетов build игнорирует файлы, заканчивающиеся на "_test.go".

Флаг -o, разрешен только при компиляции одного пакета, он заставляет build записывать полученный исполняемый файл или объект в именованный выходной файл вместо поведения по умолчанию, описанного выше.

Флаг -i устанавливает пакеты, которые являются зависимостями от цели.

Флаги сборки (build flags) используются командами build, clean, get, install, list, run и test:

Подробнее об указании пакетов см. в разделе 'go help packages'. Чтобы узнать больше о том, где установлены пакеты и бинарные файлы, запустите 'go help gopath'. Чтобы узнать больше о вызовах между Go и C/C++, запустите 'go help c'.

Примечание: build придерживается определенных соглашений, таких как те, которые описаны в 'go help gopath'. Однако не все проекты могут следовать этим правилам. Установки, которые имеют свои собственные соглашения или используют отдельную систему сборки программного обеспечения, могут использовать вызовы более низкого уровня, такие как 'go tool compile' и 'go tool link', чтобы избежать некоторых накладных расходов и проектных решений инструмента сборки.

Несмотря на то, что кроссплатформенность стала фактически стандартным атрибутом практически всех современных языков и библиотек, создавать по-настоящему кроссплатформенный продукт, всё равно было непросто. Компилируемые языки и сопутствующие библиотеки требовали сложной установки и настройки среды сборки и библиотек, а интерпретируемые — обязывали иметь или деплоить в составе необходимую версию интерпретатора. Есть немало проектов, пытающихся сделать этот процесс чуть более простым, но зачастую единственным решением оставалось устанавливать отдельный сервер и компилировать нативно.

В Go кросс-платформенность вышла на тот уровень, когда впервые можно смело отказаться от compile farms, специально настроенных dev-сред, виртуальных машин для сборки или chroot/docker-dev решений. И это ещё один серьезный game-changer, подробнее о котором я и хочу рассказать и показать на примерах
Поехали.




Как известно, в Go сознательно отказались от динамической линковки — по ряду причин, основная из которых очень схожа с обычным объяснением дизайна почти любого аспекта Go — «преимущества [динамической линковки] намного меньше её недостатков и сложности, которая она привносит в архитектуру». Что ж, главной причиной появления dynamic linking было желание экономить ресурсы — прежде всего диcковое пространство и память — которые сейчас достаточно дешевы, не только на серверах, но и в embedded-устройствах (коптеры, к примеру, несут на борту уже по 1-2 Гб RAM!). Вобщем, перечислять плюсы и минусы отдельного способа линковки — это потянет на отдельный пост, так что пока просто принимаем, как есть — в Go на выходе всегда имеем статический бинарник.

  • Linux 2.6 1 и выше — amd64, 386, arm
  • MacOS X 10.6 и выше — amd64, 386
  • Windows XP и выше — amd64, 386
  • FreeBSD 8 и выше — amd64, 386, arm
  • NetBSD — amd64, 386, arm
  • OpenBSD — amd64, 386
  • DragonFly BSD — amd64, 386
  • Plan 9 — amd64, 386
  • Google Native Client — amd64p32, 386
  • Android — arm

В Go 1.5 ожидается поддержка iOS.
Еще примечательно, что изначально поддержки Windows в Go не было — команда маленькая, и пачкать руки заниматься имплементацией кода для Windows было некому, но благодаря тому, что проект открыли для open-source разработки — порт для Windows был очень быстро написан сторонними людьми и интегрирован в официальную кодовую базу.

Хотя описанные далее процессы будут абсолютно одинаковы для всех платформ (за исключеним, разве что, Android и Native Client (NaCl), для которых нужны лишние телодвижения), далее в статье будет по-умолчанию считаться, что вы используете одну из трех самых популярных десктопных платформ — Linux, MacOS X или Windows. Кроме того, для большей простоты я буду подразумевать, что мы пишем и используем исключительно Go-код, без необходимости линковаться с С-библиотеками (и, тем самым, без необходимости использовать cgo/gcc). Есть еще отдельный кейс — когда нужно использовать ряд функций из стандартной библиотеки, завязанных на cgo, но об этом я напишу отдельной главой в конце.

Подготовка toolchain

Первый шаг, который необходимо выполнить — это собрать toolchain для нужной платформы.

Переходим в директорию с исходным кодом Go (она же $GOROOT/src, она же всегда есть у вас на машине) и пересобираем под нужную платформу, скажем Windows/amd64:

Процесс занимает на Macbook Air 2012 около 26 секунд. Скрипт make.bash — это стандартный скрипт сборки Go, которым бы вы инсталлировали Go, если бы ставили из исходников. Он собирает, собственно, Go, и всю стандартную библиотеку, только в этот раз — для платформы windows/amd64.
Также, по упомянутой выше причине, мы отключили поддержку CGO.

Значения GOOS и GOARCH

Таблица значений GOOS (если кто знает, как на Хабре сделать таблица в 50% ширины — подскажите):

OS $GOOS
Linux linux
MacOS X darwin
Windows windows
FreeBSD freebsd
NetBSD netbsd
OpenBSD openbsd
DragonFly BSD dragonfly
Plan 9 plan9
Native Client nacl
Android android

И GOARCH:

Architecture $GOARCH
x386 386
AMD64 amd64
AMD64 с 32-указателями amd64p32
ARM arm

Пример 1. Веб-сервер, написанный и собранный в Linux для Windows

Напишем простенький веб-сервер, который в Go писать проще, чем в некоторых языках/библиотеках парсить командную строку.

И соберем его для Windows 32- и 64-bit:

Думаю, не нужно говорить, что оба бинарника готовы к копированию на целевую Windows-систему и будут работать.



Пример 2. Кросс-компиляция под ARM для телефона Nokia N9

Сразу скажу, что сейчас я с embedded-девайсами плотно не работаю, поэтому могу какие-то детали не знать — так что постараюсь не углубляться в эту тему, но в целом за ситуацией с Go на embedded слежу. Вообще говоря, Go не позиционировался как язык для embedded-платформ, что, впрочем, не помешало народу активно начать его использовать в этой области. Возможно, причина в том, что embedded-индустрия сделала скачок вперед, и теперь «встраиваемое» устройство уже не означает критически малое количество ресурсов, а возможно компромиссы не в пользу экономии памяти в Go оказались гораздо менее ощутимыми на практике, но факт есть факт — для Go уже создано масса проектов вроде Gobot (robotics-фреймворк для целой кучи платформ — от Arduino, Raspberry PI и Beaglebone Back до LeapMotion, Pebble и ArDrone) или EMBD (фреймворк для работы с hobby-бордами), а PayPal уже пару лет использует Go в своем beacon-девайсе для беспроводных чекинов и платежей.

Для примера возьмем Nokia N9 (или N950, кому повезло) — и соберем вышеприведенный пример для него:




Вот так просто, да.

Автоматизируем процесс

Казалось бы, что может быть проще указания одной переменной перед go build. Но есть ситуации, когда код нужно собирать и деплоить на разные платформы по 100 раз в день. Для таких задач есть несколько проектов, для автоматизации процессов подготовки toolchain-ов и, непосредственно, сборки кода под нужную платформу.


Теперь, вместо «go build», пишем «gox»:


Можно указывать конкретный пакет или конкретную платформу:

Остальные аргументы командной строки идентичны go build. Достаточно интуитивно.

Разбираемся с CGO

Если кто-то смотрел видео с конференции GopherCon 2014, которая проходила прошлой весной в Денвере, то, возможно, помнит выступление Alan Shreve «Build Your Developer Tools in Go» — и одну из вещей, которую он говорит достаточно категорично: «не используйте кросс-компиляцию, компилируйте нативно». Дальше идет объяснение — причина в Cgo. Если вам не нужно использовать cgo — все окей. И на самом деле, очень малая часть очень специфичного кода в Go нуждается в сторонних С-библиотеках. В чем же проблема?

Проблема в том, что некоторые функции стандартной библиотеки зависят от cgo. Тоесть, если мы собираем Go с CGO_ENABLED=0, они просто не будут доступны и на этапе компиляции мы получим ошибку. Несмотря на то, что тут есть очень удобный и красивый workaround, давайте разберемся, что же именно в стандартной библиотеке зависит от cgo.

К счастью, сделать это просто:

  • crypto/x509/root_cgo_darwin.go — имплементирует одну функцию для получения корневых X.509 сертификатов в MacOS X. Если вы не используете явно эту фичу — ничего страшного, без cgo у вас все будет работать.
  • net/cgo_android/linux/netbsd/openbsd/cgo_unix_test.go — код необходимый для использования нативного DNS-резолвера в разных unix-ах. Чуть ниже подробности.
  • os/user/lookup_unix.go — функции из пакета os/user — для получения информации о текущем юзере (uid, gid, username). Используется getpwuid_r() для чтения passwd-записей
  • runtime/crash_cgo_test.go — файл с тестами для хендлинга крешей, ничего релевантного
// goLookupIP is the native Go implementation of LookupIP.
// Used only if cgoLookupIP refuses to handle the request
// (that is, only if cgoLookupIP is the stub in cgo_stub.go).
// Normally we let cgo use the C library resolver instead of
// depending on our lookup code, so that Go and C get the same
// answers.

goLookupIP фактически резолвит только по Hosts-файлу и по DNS-протоколу, что для большинства систем — ок. Но могут быть проблемы, если в системе будут использоваться нестандартные методы резолвинга имён. Полагаю, что в 99% случаев, hosts и dns будут более, чем достаточно.

  • проверку x.509 сертификатов, которая должна работать на MacOS X
  • гарантированно получать системную информацию о текущем юзере

Практическое применение

Теперь о главном — применении на практике. Я использовал в продакшене пока только три схемы — «сборка на darwin/amd64 -> деплой на linux/386», «linux/amd64 -> linux/386» и «linux/amd64 -> windows/amd64». Это продукты, которые уже больше года полноценно работают. Третий случай (деплой на windows) тогда меня вообще застал врасплох — был сервер, успешно бегущий на Linux, и тут вдруг резко понадобилось его запускать на Windows. Причем «вот срочно надо». Вспоминая бессонные ночи опыта с кросс- — да что там кросс, просто с компиляцией Qt для деплоя на Windows — 60-секундный процесс «гуглим как это делается → сборка toolchain → перекомпиляция проекта → деплой на windows» — стал просто шоком, я тогда даже не поверил глазам.

Но тут возникает следующий момент — раз кросс-компиляция и деплой становятся такими простыми и быстрыми, появляется стимул все зависимости от файлов — будь-то конфиги, сертификаты или что угодно еще — встраивать в бинарник тоже. Впрочем, это достаточно простая задача, даже для сторонних библиотек, благодаря эффективному использованию io.Reader интерфейса и пакету go-bindata, но это уже тема для отдельной статьи.

Надеюсь, ничего из главного не упустил.
Но в целом это на самом деле очень существенная разница со всем предыдущим опытом кросс-сборки. Если честно, я до сих пор не привык к этой перемене. Больше не нужны виртуалки с настроенной dev-средой, не нужны докер-имиджи для сборки — да вообще dev-environment отпадает как таковой. Это слишком резкий game changer, чтобы так быстро привыкнуть.


Компилятор Go занимает важное место в экосистеме Go. Компиляция — необходимый этап создания исполняемого двоичного кода. Компилятор проходит долгий путь: его пишут на C, переносят на Go и постоянно оптимизируют.

Компилирование Go состоит из четырёх фаз, которые можно объединить в два этапа:

  • На первом выполняется анализ исходного кода и по мере синтаксического разбора создаётся абстрактная синтаксическая структура исходного кода, которая называется АСД (абстрактное синтаксическое дерево).
  • На втором этапе вместе с многочисленными оптимизациями происходит трансформация представления исходного кода в машинный код.


Для лучшего понимания используем простую программу:

Первая фаза предельно проста. Её описание можно найти в README:

В первой фазе компиляции исходный код маркируется (это лексический анализ), затем анализируется синтаксически и для каждого исходного файла создаётся синтаксическое дерево.

Лексический анализатор будет первым пакетом, запущенным для маркирования. Исходный код здесь. Ниже приведён результат:


После маркирования проводится синтаксический анализ и строится синтаксическое дерево.

Преобразован и е в абстрактное синтаксическое дерево можно вывести на экран командой go tool compile -W :


На первой фазе также проводятся оптимизации. Например, встраивание. В нашем примере метод add уже может быть встроенным, так как никаких инструкций CALLFUNC с методом add мы здесь не видим. Теперь снова выполним команду с флагом -l для отключения встраивания:


АСД позволяет компилятору перейти к низкоуровневому промежуточному представлению SSA (Static Single Assignment — статическое одиночное присваивание).

Создание формы статистического одиночного присваивания — это фаза оптимизации: устранение мёртвого кода, замена выражений на константы и т.д. Код SSA можно вывести командой GOSSAFUNC=main go tool compile main.go && open ssa.html , с помощью которой создаётся HTML-документ со всеми проходами, сделанными в пакете SSA:


SSA находится во вкладке start:


Переменные a и b здесь выделены, как и условие if : позже можно отследить, как меняются эти строки. Код также показывает, каким образом компилятор управляет функцией println , которая разбивается на четыре: printlock , printint , printnl , printunlock . Компилятор автоматически добавляет блокировку и, в зависимости от типа аргумента, вызывает соответствующий метод для корректного вывода.

В нашем примере a и b известны на стадии компиляции, поэтому компилятор может посчитать конечный результат и отметить переменные как ненужные. Проход opt оптимизирует эту часть:


v11 здесь заменён результатом добавления v4 и v5 , обозначенных как мёртвый код. Проход opt deadcode удалит его:


Что касается условия if , проход opt отмечает константу true как мёртвый код, он будет удалён:


Затем другой проход упростит поток управления, отметив ненужный блок и условие как бесполезные. Эти блоки будут после удалены другим проходом, работающим с мёртвым кодом:


После всех проходов компилятор Go создаёт промежуточный код на ассемблере Go:


На следующей фазе создаётся машинный код для бинарного файла.

Заключительный этап компиляции — это создание объектного файла ( main.o в нашем случае). Его можно дизассемблировать с помощью команды go tool objdump . Ниже схема работы компилятора:



После создания объектного файла можно перейти непосредственно к компоновщику. Используйте команду go tool link — и ваш двоичный код готов!

Компилятор Go занимает важное место в экосистеме Go. Компиляция — необходимый этап создания исполняемого двоичного кода. Компилятор проходит долгий путь: его пишут на C, переносят на Go и постоянно оптимизируют.

Фазы компиляции

Компилирование Go состоит из четырёх фаз, которые можно объединить в два этапа:

  • На первом выполняется анализ исходного кода и по мере синтаксического разбора создаётся абстрактная синтаксическая структура исходного кода, которая называется АСД (абстрактное синтаксическое дерево).
  • На втором этапе вместе с многочисленными оптимизациями происходит трансформация представления исходного кода в машинный код.


README компилятора

Для лучшего понимания используем простую программу:

Синтаксический разбор

Первая фаза предельно проста. Её описание можно найти в README:

В первой фазе компиляции исходный код маркируется (это лексический анализ), затем анализируется синтаксически и для каждого исходного файла создаётся синтаксическое дерево.

Лексический анализатор будет первым пакетом, запущенным для маркирования. Исходный код здесь. Ниже приведён результат:


как маркируется исходный код в Go

После маркирования проводится синтаксический анализ и строится синтаксическое дерево.

Преобразование в АСД

Преобразование в абстрактное синтаксическое дерево можно вывести на экран командой go tool compile -W :


Токенизация

На первой фазе также проводятся оптимизации. Например, встраивание. В нашем примере метод add уже может быть встроенным, так как никаких инструкций CALLFUNC с методом add мы здесь не видим. Теперь снова выполним команду с флагом -l для отключения встраивания:


АСД позволяет компилятору перейти к низкоуровневому промежуточному представлению SSA (Static Single Assignment — статическое одиночное присваивание).

Генерация SSA

Создание формы статистического одиночного присваивания — это фаза оптимизации: устранение мёртвого кода, замена выражений на константы и т.д. Код SSA можно вывести командой GOSSAFUNC=main go tool compile main.go && open ssa.html , с помощью которой создаётся HTML-документ со всеми проходами, сделанными в пакете SSA:


Проходы SSA

SSA находится во вкладке start:


Код SSA

Переменные a и b здесь выделены, как и условие if : позже можно отследить, как меняются эти строки. Код также показывает, каким образом компилятор управляет функцией println , которая разбивается на четыре: printlock , printint , printnl , printunlock . Компилятор автоматически добавляет блокировку и, в зависимости от типа аргумента, вызывает соответствующий метод для корректного вывода.

В нашем примере a и b известны на стадии компиляции, поэтому компилятор может посчитать конечный результат и отметить переменные как ненужные. Проход opt оптимизирует эту часть:


SSA, проход opt

v11 здесь заменён результатом добавления v4 и v5 , обозначенных как мёртвый код. Проход opt deadcode удалит его:


SSA, проход opt deadcode

Что касается условия if , проход opt отмечает константу true как мёртвый код, он будет удалён:


Удаление лишнего true

Затем другой проход упростит поток управления, отметив ненужный блок и условие как бесполезные. Эти блоки будут после удалены другим проходом, работающим с мёртвым кодом:


Удаление ненужного потока управления

После всех проходов компилятор Go создаёт промежуточный код на ассемблере Go:


ассемблерный код Go

На следующей фазе создаётся машинный код для бинарного файла.

Создание машинного кода

Заключительный этап компиляции — это создание объектного файла ( main.o в нашем случае). Его можно дизассемблировать с помощью команды go tool objdump . Ниже схема работы компилятора:


go tool compile


go tool objdump

После создания объектного файла можно перейти непосредственно к компоновщику. Используйте команду go tool link — и ваш двоичный код готов!


Как создать исполняемую программу в Golang?

Вам нужно записать исходные файлы Go в пакеты. В Go есть два вида пакетов:

  • Пакеты, скомпилированные в исполняемые программы;
  • Пакеты, скомпилированные в общую библиотеку.

Создадим подпапку под названием hello внутри директории $GOPATH/src . В Листинге 1 программа “Hello, World” демонстрирует базовые примеры написания программ на Go.

Давайте разберем данную программу, чтобы понять базовые аспекты создания программ на Go. В отличие от языков программирования на подобии C, Go не требует размещения точки с запятой (;) под конец оператора. Запишем исходный файл main.go и организуем его код в пакет main .

Объявление пакета уточняет пакет, к которому относится исходный файл Go. Здесь мы уточняем, что файл main.go является частью пакета main . Обратите внимание, что все файлы источника в одной директории (директория пакета) нужно объявлять с тем же названием пакета. Результатом компиляции пакета main станет исполняемая программа.

Оператор import используется для импорта пакетов (общих библиотек), таким образом можно повторно использовать функции импортированного пакета. Здесь мы импортируем пакет fmt , предоставленный стандартной библиотекой. Пакеты стандартной библиотеки можно найти в месте расположения GOROOT (директория инсталляции Go).

Мы используем ключевое слово func для объявления go функции, за которым следует название функции. Функция main является специальной функцией, что работает как точка входа для программы исполнения. В пакете main должна быть функция main , что работает как точка входа для программ исполнения. Мы используем функцию Println из пакета fmt для вывода данных.

Самое время создать и запустить программу, чтобы посмотреть на результат. Это можно сделать с помощью инструмента go . Перейдем в директорию пакета в окне командной строки и запустим следующую команду для компиляции программы:

Команда build компилирует исходный пакет и генерирует исполняемую программу с названием директории, что содержит исходные файлы Go для пакета main . Так как мы используем директорию под названием hello , командой исполнения будет hello (или hello.exe для Windows). Запустите команду hello из директории hello в окне командной строки, чтобы посмотреть на вывод.

Вы должны увидеть следующий результат:

В дополнение к команде go build для компиляции источника и помещения бинарного результата в директорию bin , что внутри GOPATH , вы можете использовать go install .

Теперь можно запустить команду исполнения из директории bin внутри GOPATH . Если вы добавили $GOPATH/bin в свою переменную среды PATH вы можете запустить программу из любого места в окне командной строки.

Если вы хотите просто скомпилировать и запустить программу, можете использовать команду go run вместе с название файла программы.

Читайте также: