1. Навіщо потрібна кроскомпіляція
Кроскомпіляція звучить як «чарівництво рівня “збери Windows на Mac, не виходячи з Vim”». На практиці все значно прозаїчніше: у вас один проєкт, але користувачі працюють на різних ОС і з різними CPU. Хочемо зібрати бінарник для Linux-сервера, для Windows-ноутбука колеги та для маленького ARM-девайса — і не переписувати код. У Go більшість цього вирішується двома змінними середовища: GOOS і GOARCH.
Щоб не плутатися в термінах, тримайте в голові просту ідею. Компілятор завжди створює бінарник «під щось конкретне», і це «щось» ми називаємо target (ціль). А середовище, де ви запускаєте go build, називається host (хост). Ці слова часто траплятимуться далі, і ми закріпимо їх на прикладах.
Host vs target
Коли ви набираєте go build, це відбувається на вашій машині або в CI. Саме це і є host: операційна система й архітектура, на яких працює інструмент збірки. Але результат збірки — бінарник — призначений для виконання на певній платформі. Це target: операційна система й архітектура, під які скомпільовано програму.
Уявіть, що ви друкуєте інструкцію на принтері. Принтер стоїть у вас удома (host), а інструкцію ви друкуєте для друга, у якого інша модель пристрою (target). Місце друку не змінює зміст тексту — ви просто заздалегідь обираєте, під який пристрій друкувати інструкцію. У Go ви «обираєте пристрій» через GOOS/GOARCH.
Наочно це можна уявити так:
flowchart LR
A[Хост: де запускаємо go build] -->|GOOS/GOARCH задають| B[Ціль: під що збираємо бінарник]
B --> C[Бінарник: запускається лише на цільовій платформі]
І одразу практичний наслідок: якщо ви зібрали бінарник під windows/amd64, то на Linux ви його «просто так» не запустите. Бінарник чесно спробує бути Windows-програмою, а Linux чесно скаже: «Я не Windows».
2. GOOS/GOARCH: вибір платформи збірки
Що таке GOOS і GOARCH
GOOS — це цільова операційна система (operating system), а GOARCH — цільова архітектура процесора (architecture). Разом вони задають target-платформу. Наприклад, windows/amd64 означає «Windows на 64-бітній x86-архітектурі», а linux/arm64 — «Linux на ARM64».
Якщо ви бачили, що в екосистемі Go ці параметри трапляються не лише під час збірки, а й у документації, це нормально: навіть сторінки стандартної бібліотеки можуть враховувати GOOS/GOARCH у частинах API, залежних від платформи. У тестових даних сайту Go можна побачити параметри на кшталт ?GOOS=windows&GOARCH=amd64.
Мінімальний набір платформ, з яким зазвичай стикаються в повсякденній розробці, зручно тримати у вигляді таблиці — і не намагатися вивчити весь зоопарк одразу. Він справді великий:
|
|
Що це зазвичай означає | Де трапляється |
|---|---|---|---|
|
|
типовий сервер або контейнер | бекенд, CI |
|
|
ARM-сервери, Raspberry Pi, деякі хмари | IoT, ARM-cloud |
|
|
macOS на Apple Silicon | сучасні Mac |
|
|
Windows 64-біт | десктоп |
Іноді ви можете зустріти й екзотичніші комбінації. Наприклад, у новинах про Go згадують платформи на кшталт WASI (GOOS=wasip1, GOARCH=wasm). Але для «мінімального набору» нам достатньо впевнено жити у чотирьох рядках таблиці вище.
Як задати GOOS/GOARCH під час збірки
Тепер — простий і чесний приклад того, як це виглядає на практиці. GOOS і GOARCH — це змінні середовища. Отже, їх можна задати «на одну команду», не змінюючи нічого глобально. Для новачка це найбезпечніший режим: менше шансів випадково зібрати щось не під ту платформу.
У Linux чи macOS зазвичай пишуть так — в один рядок перед командою:
GOOS=windows GOARCH=amd64 go build -o todo.exe ./cmd/todo
У Windows (PowerShell) синтаксис інший, але ідея та сама: ви задаєте значення в середовищі й запускаєте go build. Тут ми не заглиблюватимемося у відмінності оболонок — важливо зрозуміти сам принцип: значення GOOS/GOARCH беруться із середовища на момент запуску go build.
Чому -o тут доречний? Тому що ми збираємо один main-пакет і очікуємо один вихідний файл. А ще ми одразу бачимо корисний нюанс: для Windows зазвичай зручно додати розширення .exe до імені файла. Це не вимога компілятора за будь-яку ціну; це вимога середовища й очікувань користувачів. Windows-користувач без .exe починає губитися й шукати помилку.
І так, важливий побутовий факт: якщо ви на macOS зібрали todo.exe, то це ще не означає, що ви можете одразу запустити його подвійним клацанням. Ви зібрали «під Windows», а macOS — не Windows. Це не помилка Go; це просто реальність.
Як дізнатися target зсередини бінарника
З кроскомпіляцією є типова проблема: ви швидко назбирали кілька бінарників, а потім дивитеся на папку й думаєте: «Так… а оцей — під що? Я або геній, або просто забув підписати файли». Щоб не залежати від імені файла, яке можна перейменувати, корисно зробити так, щоб сам бінарник міг сказати, під яку платформу його зібрано.
У Go це робиться через пакет runtime: у ньому є значення runtime.GOOS і runtime.GOARCH. Важлива тонкість: це не «ОС, на якій зараз виконується go build». Це значення, вшиті в бінарник під час збірки, тобто вони описують target.
Мініприклад — його можна запускати як окрему іграшку, щоб відчути ідею:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("ціль: %s/%s\n", runtime.GOOS, runtime.GOARCH) // ціль: linux/amd64
}
Якщо ви зберете цей код під windows/amd64, він надрукує ціль: windows/amd64 — навіть якщо збирали ви його на Linux або macOS. Саме це нам і потрібно для діагностики артефактів.
4. Додаємо target у --version
Тепер ми зробимо маленьке, але дуже практичне покращення нашого навчального застосунку todo. У попередній лекції в нас уже з’явився пакет internal/buildinfo, де лежать var Version, var Commit, var Date, а main уміє за прапорцем --version друкувати цю інформацію та виходити.
Тепер додамо туди target-платформу. Сенс у тому, щоб команда:
todo --version
показувала не лише версію й коміт, а й, наприклад, linux/amd64 або windows/amd64. Тоді будь-яка людина, яка отримала бінарник, може перевірити, що це взагалі таке, без зайвих хитрощів.
Оновімо internal/buildinfo: додамо Target()
Тут важливо зрозуміти дизайн-ідею: build-інформація має жити в одному місці, щоб її не розмазувати по проєкту. Тому target ми теж додамо в buildinfo. Там же ми й форматуватимемо вивід.
package buildinfo
import "runtime"
// Target повертає платформу, під яку зібрано бінарник.
func Target() string {
return runtime.GOOS + "/" + runtime.GOARCH
}
Код короткий, майже «занадто простий», але саме такі речі й роблять продукт зручним: користувачу не потрібно вгадувати — ви самі все сказали.
Зберемо єдиний формат версії
Тепер хочеться звести все в один рядок, щоб main був максимально простим: якщо попросили версію — друкуй готове й виходь. Це зменшує шанс, що в одному місці ви друкуєте версію так, а в іншому — інакше.
package buildinfo
import "fmt"
var Name = "todo"
var Version = "dev"
var Commit = "none"
func FullVersion() string {
return fmt.Sprintf("%s %s (%s) %s", Name, Version, Commit, Target())
}
Тут ми використали var, бо Version і Commit ми хочемо вміти підставляти через -ldflags -X (як обговорювали раніше). А Target() береться з runtime і автоматично правильний для зібраного бінарника.
Підключимо це в main
Зараз буде важливий момент про UX командного рядка: --version має працювати швидко й не запускати «основну логіку» застосунку. Це особливо важливо, коли основна логіка може вимагати конфіг, файли, мережу — що завгодно. Версія — це діагностика, а не виконання бізнес-процесу.
package main
import (
"flag"
"fmt"
"example.com/todo/internal/buildinfo"
)
func main() {
showVersion := flag.Bool("version", false, "показати версію й вийти")
flag.Parse()
if *showVersion {
fmt.Println(buildinfo.FullVersion()) // todo dev (none) linux/amd64
return
}
fmt.Println("todo: застосунок запущено")
}
Тепер будь-який ваш бінарник сам повідомляє, що він собою являє. Це особливо зручно, коли у вас на руках два файли: один під Linux, другий під Windows, і ви не хочете вгадувати за іменем.
5. Практика: розширення .exe, запуск і очікування
Кроскомпіляція часто ламає мозок новачку не тому, що вона складна, а тому, що очікування не збігаються з реальністю. Здається: «Я ж щойно зібрав, чому не запускається?» — а тому, що ви зібрали не під цю платформу. Тому корисно одразу тримати три практичні правила.
Перше правило: ім’я бінарника має натякати на платформу. Навіть якщо ви додали target у --version, людині все одно зручніше одразу бачити з імені файла, що вона завантажила. Зазвичай в імені відображають хоча б GOOS і GOARCH, а на Windows додають .exe.
Друге правило: якщо ви зібрали під Windows, запускати треба на Windows. Якщо зібрали під Linux — на Linux. Це звучить очевидно, але вперше мозок опирається й каже: «Ну комп же один і той самий…». Ні, ОС — частина платформи.
Третє правило: не ускладнюйте кросзбірку завчасно. Так, існують тонкощі, наприклад cgo і зовнішні C-залежності, але в «мінімальному наборі» ми вважаємо, що в нас звичайний Go-застосунок, який використовує стандартну бібліотеку, і кросзбірка для нього — це справді дві змінні середовища. Якщо колись ви упретеся в нюанси, ви вже розумітимете базову модель і зможете точково розібратися, що саме заважає.
6. Типові помилки під час кроскомпіляції GOOS/GOARCH
Помилка № 1: плутати host і target та чекати, що «зібране» обов’язково запускатиметься на поточній машині.
Дуже частий сценарій: ви на macOS зібрали GOOS=windows, отримали файл, спробували запустити — і нічого. Це не «баг збірки», а коректна поведінка: ви зробили Windows-програму, а запускаєте на macOS. Лікується поверненням до моделі host/target: де збираємо — не те саме, що де запускаємо.
Помилка № 2: переплутати GOOS і GOARCH місцями або задати неможливу пару.
Коли в голові ще каша, легко написати щось на кшталт GOOS=amd64 GOARCH=windows. Go зазвичай швидко скаже, що так не можна, але ви витратите час, з’ясовуючи, чому він свариться. Корисна звичка — тримати під рукою коротку таблицю популярних комбінацій і не намагатися вгадувати.
Помилка № 3: забути про .exe для Windows і потім дивуватися, чому це не запускається в користувача.
Технічно бінарник може існувати й без розширення, але користувач Windows очікує .exe, і багато інструментів та Провідник орієнтуються саме на нього. Тому якщо ціль — Windows, краще одразу на етапі -o дати зрозуміле ім’я з .exe. Це не забаганка Go, а повага до платформи.
Помилка № 4: не вміти перевірити, під що зібрано бінарник, і жити за принципом «здається, це Linux».
Коли артефактів стає кілька, вгадування ламається миттєво. Рішення просте: додайте в --version друк runtime.GOOS/runtime.GOARCH (наприклад, через buildinfo.Target()), і перевірка перетворюється на одну команду. Ви перестаєте вгадувати — бінарник сам усе каже.
Помилка № 5: збирати різні платформи, але друкувати однакову «версію» без target, а потім плутати звіти про помилки.
Якщо користувач пише: «У мене todo v1.2.3 падає», а у вас немає інформації про платформу, ви будете ставити зайві запитання. Набагато зручніше, коли версія виглядає як «todo 1.2.3 (abc123) windows/amd64»: це одразу економить час і вам, і користувачу.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ