1. Навіщо розбиратися з go build, якщо в IDE є кнопка Run
Коли ви тільки починаєте, IDE схожа на рятівне коло: натиснули зелену кнопку — і програма «якось» працює. Але щойно проєкт стає більшим за «Hello, World», вам уже потрібно свідомо керувати збіркою. Іноді треба зібрати бінарник і надіслати його другові. Іноді — перевірити, чи збираються всі пакети. Іноді — зрозуміти, чому в колеги «не збирається», а у вас — так. У цей момент go build перестає бути заклинанням і стає інструментом, яким ви справді керуєте.
У Go збірка — це не про «скомпілювати один файл», а про «зібрати пакети й залежності, а якщо серед них є main — згенерувати виконуваний файл». І це дуже логічно: у реальному проєкті код майже завжди розкладено по папках, а не живе в одному величезному main.go, який страшно відкривати без каски.
2. Пакети: як Go мислить збірку
Важливо звикнути до однієї думки: пакет у Go — це директорія з .go-файлами, які оголошують один і той самий package.
Тобто Go дивиться на проєкт приблизно так:
- у кожній папці — один пакет;
- пакет може імпортувати інші пакети;
- збірка стартує з «цілі» (target), яку ви вказали: поточного пакета, конкретного пакета або набору пакетів за шаблоном.
Уявімо спрощене дерево нашого навчального проєкту todo (CLI-менеджер завдань). Воно може виглядати так:
todo/
go.mod
cmd/
todo/
main.go
internal/
app/
app.go
task/
task.go
І ключова ідея: cmd/todo — це main-пакет (програма), а internal/app і internal/task — звичайні бібліотечні пакети (компоненти).
Щоб це було зовсім «в лоб», ось два мініфайли.
cmd/todo/main.go — це програма:
package main
import "fmt"
func main() {
fmt.Println("todo запущено") // Повідомлення про запуск todo
}
internal/task/task.go — це бібліотечний пакет (він сам не запускається):
package task
type Task struct {
Title string
Done bool
}
Збірка main-пакета дасть вам виконуваний файл. Збірка бібліотечного пакета — лише перевірить компіляцію і збереже результат у кеші (про кеш поговоримо нижче). Окремого «exe для бібліотеки» не буде — і це нормально.
3. Цілі збірки: поточний пакет, main і бінарник
Збірка без аргументів і ціль .
Коли ви запускаєте go build без аргументів, ви фактично кажете: «збери пакет у поточній директорії». Те саме можна записати явно через . — це просто прозоріший варіант.
Якщо ви перебуваєте в директорії cmd/todo, то команди еквівалентні:
go build
go build .
Обидві означають: «збери пакет, який лежить ось тут».
Тепер важлива деталь, яка часто дивує новачків: якщо поточний пакет — не main, то після go build ви можете не побачити жодного нового файла в папці. І це не збій, а нормальна поведінка: Go зібрав пакет, перевірив, що він компілюється, і використав кеш.
Це зручно: ви можете швидко перевірити, що пакет internal/task компілюється, не створюючи зайвих бінарників:
cd internal/task
go build .
У вас не з’явиться task.exe (і це нормально). Але якщо в коді є помилка — компілятор чесно покаже її.
Чому бінарник з’являється лише для main
Перш ніж говорити про ./..., закріпімо «магічний маркер» програми: package main + func main().
Якщо пакет називається main, але функції main немає, програма не вважається коректною точкою входу. Якщо функція main є, але пакет не main, — теж не вважається. Це як ключ і замок: окремо вони марні, разом — запускають застосунок.
Мініприклад: «майже програма, але ще ні».
package main
import "fmt"
// Немає func main() — немає чого запускати.
func Start() {
fmt.Println("старт") // цей код сам по собі не запуститься
}
Якщо зібрати таке як програму, компілятор не буде в захваті: він очікує точку входу.
А ось мінімальна «правильна програма»:
package main
import "fmt"
func main() {
fmt.Println("готово") // повідомлення про завершення
}
Висновок для практики дуже простий: якщо ви хочете отримати бінарник, збирайте main-пакет. Усе інше — це збірка компонентів і перевірка, що все компілюється.
4. ./...: усі пакети рекурсивно, а не «одна велика програма»
Запис ./... — один із найкорисніших у Go-інструментах. Він означає: усі пакети в поточній директорії та в усіх вкладених директоріях.
Тобто, якщо ви стоїте в корені модуля todo/, то:
go build ./...
означає: «пройдися деревом проєкту і спробуй зібрати кожен пакет».
Це часто використовують як швидку перевірку: «проєкт узагалі компілюється?» — особливо перед комітом або перед запуском тестів.
І ось тут найчастіша пастка очікувань: ./... не означає «збери один спільний бінарник з усього проєкту». У Go немає ідеї автоматично склеїти всі пакети в одну програму. Програма — це конкретний main-пакет (або кілька main-пакетів, якщо у вас кілька команд).
Якщо у вашому проєкті один main у cmd/todo, тоді go build ./... справді збиратиме й його. Але результатом цієї команди майже завжди є не «ось вам готовий релізний бінарник», а просто факт: «усі пакети успішно компілюються».
Щоб це вклалося в голові, корисно порівняти команди в таблиці:
| Команда | Що є ціллю | Що ви хочете отримати | Типовий результат |
|---|---|---|---|
|
поточний пакет | «збери те, де я стою» | бінарник (якщо це main), або просто перевірка й кеш |
|
поточний пакет (явно) | те саме, але читабельніше | те саме |
|
усі пакети рекурсивно | «нехай усе компілюється» | зазвичай без «нового файла», але з помилками, якщо вони є |
|
конкретний пакет | «збери команду todo» | бінарник todo у поточній директорії |
Якщо коротко й чесно: ./... — це «перевірка здоров’я дерева пакетів», а не «виробництво одного артефакту».
5. -o: назва і місце для бінарника
Коли ви збираєте main-пакет, Go за замовчуванням кладе бінарник у поточну директорію й називає його за іменем останнього елемента шляху пакета. Наприклад, якщо ви запускаєте збірку з кореня модуля:
go build ./cmd/todo
то бінарник опиниться у поточній директорії (тобто в корені проєкту) і називатиметься todo (або todo.exe на Windows).
Це може бути нормальним варіантом, але часто хочеться акуратніше: наприклад, складати бінарники в ./bin/, щоб не захаращувати корінь проєкту. Ось тут і з’являється -o.
Приклад «збери й поклади в bin»:
go build -o bin/todo ./cmd/todo
Якщо ви працюєте у Windows, розширення зазвичай вказують явно:
go build -o bin/todo.exe ./cmd/todo
Сенс -o дуже приземлений: ви керуєте вихідним файлом. Не «десь там IDE сховала», а прямо руками: ось шлях, ось ім’я.
Ще один корисний сценарій: ви хочете збирати бінарник з назвою, зрозумілою людині, навіть якщо пакет називається інакше. Наприклад, пакет ./cmd/todo може називатися todo, а ви хочете бінарник todo-cli:
go build -o bin/todo-cli ./cmd/todo
Тоді у вас два імені: назва пакета й назва бінарника. Тому в житті зазвичай тримають їх близькими. Але коли потрібно — -o вас рятує.
Окремий нюанс — без заглиблення, але щоб не лякатися: якщо ви спробуєте використати -o для збірки одразу багатьох пакетів (наприклад, ./...), Go може вилаятися, бо «не можна вивести багато результатів в один файл». Це логічно: один файл — один бінарник.
6. Що робить go build: залежності, лінкування та build cache
Корисно хоча б на рівні схеми розуміти, що відбувається під час збірки. Go робить це доволі швидко, але всередині все одно є етапи.
Спрощено процес виглядає так:
flowchart TD
A[Ви запускаєте go build] --> B[Go знаходить цільовий пакет]
B --> C[Go читає imports і будує граф залежностей]
C --> D["Компілює пакети знизу вгору"]
D --> E{Є main-пакет?}
E -- ні --> F[Результати в build cache, бінарника немає]
E -- так --> G[Лінкування: збирання виконуваного файлу]
G --> H["З’являється бінарник (якщо не задано -o — у поточній папці)"]
Слово «лінкування» звучить грізно (ніби зараз прийде лінкер і спитає, чи готові ви до дорослого життя). На практиці це просто фінальний крок: «склеїти» скомпільовані шматки в один виконуваний файл.
І ось тут є дуже важлива річ, яка робить Go приємним: build cache (кеш збірки). Він пояснює феномен:
- перший go build — трохи довше,
- другий go build — помітно швидше,
- третій — узагалі «миттєво, я навіть не встиг кліпнути».
Чому? Бо якщо Go бачить, що пакет і його залежності не змінилися, він повторно використовує результати компіляції з кешу. Ви ніби отримуєте мінітурборежим, нічого не роблячи.
Це не магія, а інженерна оптимізація. Але, як і будь-яка магія, вона викликає питання, якщо ви не знаєте, що вона існує: «Чому в мене не з’явився файл?» або «Чому воно збирається так швидко, може, воно взагалі нічого не робить?». Робить. Просто розумно.
7. Мінісценарій для todo: які команди запускати
Щоб знання не залишилися теорією, прив’яжімо їх до нашого навчального застосунку todo.
Уявіть три типові стани розробника:
Перший стан — «я змінював main.go, хочу запустити програму». Логіка проста: вам потрібен бінарник, отже ви збираєте main-пакет. Якщо ви перебуваєте в cmd/todo, то вистачить:
go build
Другий стан — «я змінював код у internal/..., хочу переконатися, що проєкт повністю компілюється». Тут часто корисніше:
go build ./...
Це дасть вам упевненість, що ви не зламали якийсь пакет, який не бере участі у вашому локальному сценарії запуску, але потрібний для тестів або іншої команди.
Третій стан — «я хочу акуратний бінарник у bin/». Тоді:
go build -o bin/todo ./cmd/todo
І все: корінь проєкту чистий, артефакти в одному місці, далі хоч скрипт пишіть, хоч запускайте вручну.
8. Типові помилки
Помилка №1: очікувати, що go build ./... створить «один спільний бінарник».
Це цілком природне очікування, якщо ви прийшли зі світу, де є «зібрати проєкт цілком». У Go ./... означає «усі пакети рекурсивно», тобто перевірку, чи компілюється дерево пакетів. Бінарник створюється лише під час збірки конкретного main-пакета, а не «всього підряд».
Помилка №2: плутати пакет із файлом і мислити категорією «зіберу main.go».
Так, технічно go build уміє приймати список файлів, але це радше аварійний варіант для дрібних експериментів. Нормальний Go-проєкт збирають пакетами: go build ./cmd/todo, go build ., go build ./.... Щойно ви починаєте мислити директоріями та шляхами імпорту, багато що стає простішим і передбачуванішим.
Помилка №3: запускати go build у «не тій» папці й дивуватися відсутності бінарника.
Якщо ви випадково опинилися в internal/task і виконали go build, бінарника й не буде — це бібліотечний пакет. У такій ситуації проблема не в збірці, а в цілі: щоб отримати бінарник, збирайте main-пакет (наприклад, ./cmd/todo) або перейдіть у його директорію.
Помилка №4: використовувати -o там, де ви насправді збираєте багато пакетів.
Прапорець -o — це про керування вихідним файлом. Якщо ви збираєте один main-пакет — чудово. Але якщо ви намагаєтеся поєднати -o і ./..., ви фактично просите Go «виплюнути багато результатів в один файл», а це вже не має сенсу. У таких випадках спочатку визначтеся, який конкретно main-пакет ви хочете зібрати.
Помилка №5: думати, що «якщо вдруге швидко, значить нічого не зібралося».
Це класичний конфлікт людини з кешем. Go справді може перебудувати проєкт за частки секунди, бо повторно використовує результати компіляції пакетів. Якщо код не змінювався, швидка збірка — ознака здорової роботи, а не халтури компілятора.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ