1. Коли і навіщо потрібен Delve
Налагоджувач часто сприймають як чарівну кнопку «зроби, щоб усе запрацювало». На жаль, він не виправляє код, а лише дає змогу спостерігати за його виконанням у сповільненому режимі. Та в цьому сповільненому режимі є велика сила: можна зупинитися рівно в потрібному рядку, подивитися реальні значення змінних і зрозуміти, на якому кроці ваші очікування розійшлися з реальністю.
Delve (команда dlv) — стандартний налагоджувач для Go. Він корисний, коли ви вже приблизно знаєте, де проблема, наприклад за стеком викликів, але ще не розумієте, чому значення стали саме такими. Водночас Delve доступний не завжди: через обмеження середовища, політику компанії або просто через те, що у вас Windows, а сьогодні все проти вас. Тому навички читання стеку викликів і діагностичного виводу все одно мають бути у вас під рукою.
Проста модель роботи
Щоб Delve не здавався чимось містичним, корисно тримати в голові просту модель. Зазвичай ваша програма біжить уперед без зупинок. Налагоджувач же дає змогу ставити «стоп-кадри» (breakpoints), щоб зупинити виконання, а потім або продовжити рух, або крокувати рядок за рядком. У момент зупинки можна розглядати локальні змінні, параметри функцій, стек викликів і навіть іноді обчислювати вирази.
Важлива психологічна деталь: налагодження — це не «дивимося все підряд», а «перевіряємо одну гіпотезу». Якщо ви намагаєтеся одночасно стежити за 20 змінними, мозок перегрівається швидше, ніж ноутбук під літнім сонцем. Тому ми вчитимемося ставити точки зупину ближче до місця порушення інваріанта й дивитися лише на кілька ключових значень.
2. Базова робота в Delve
Мінімальний запуск
У реальному житті Delve запускають або через IDE (наприклад, GoLand/IntelliJ, VS Code), або безпосередньо через командний рядок. У цій лекції нам важливіше зрозуміти принципи, тому говоритимемо про dlv як про інструмент з інтерактивною консоллю: ви запускаєте програму під налагоджувачем і спілкуєтеся з ним командами. IDE робить те саме, просто замість команд у вас кнопки, панелі й таблиці.
Найчастіший сценарій — запустити поточний пакет як звичайну програму, але під контролем налагоджувача. У терміналі це зазвичай виглядає як «запуск налагоджувальної версії», після чого ви потрапляєте в консоль Delve і вже там ставите точки зупину та керуєте кроками. Якщо в програми є аргументи командного рядка, їх теж можна передати, але сьогодні нам достатньо самої ідеї: ми можемо стартувати й зупинитися в main.
Точка зупину
Точка зупину (breakpoint) — це місце, де виконання призупиняється. Ви кажете налагоджувачу: «коли виконання дійде до цього рядка або до входу в цю функцію, постав програму на паузу». У цей момент можна спокійно подивитися змінні й зрозуміти, що відбувається. Це набагато точніше, ніж «розставити 15 fmt.Printf і потім читати кілометровий лог як роман на 1200 сторінок».
Є два практичні способи ставити точки зупину: за назвою функції та за координатою файл:рядок. На початку зручно ставити точку зупину на main.main, щоб упіймати програму одразу після старту. Далі її варто переносити ближче до проблемної логіки: прямо перед небезпечною операцією — індексацією, розіменуванням вказівника, діленням, type assertion — або перед гілкою if, яка здається підозрілою.
Невелика таблиця «що зазвичай потрібно новачку в точках зупину» — це не «вивчи напам’ять», а «знай, що таке існує»:
| Намір | Приклад команди в dlv (ідея) | Що відбувається |
|---|---|---|
| Зупинитися на вході у функцію | |
Пауза при вході в main |
| Зупинитися на конкретному рядку | |
Пауза перед виконанням рядка |
| Подивитися список точок зупину | |
Покаже, що і де поставлено |
| Видалити точку зупину | |
Прибере точку зупину |
Одразу важливе уточнення: «точка зупину на рядку» зазвичай означає «зупинитися перед виконанням цього рядка». Це нормально: ви встигаєте подивитися значення до того, як операція станеться.
Кроки виконання: next, step, finish
Коли програма зупинилася на точці зупину, виникає питання: що далі? Можна продовжити виконання до наступної точки зупину, а можна крокувати. Крокування — це головний кайф налагоджувача: ви бачите, як змінюються значення змінних, і можете впіймати момент, коли все пішло не туди.
Є два фундаментальні типи кроку: «переступити рядок» і «зайти всередину виклику функції». У налагоджувачах це зазвичай називається next і step. Якщо на поточному рядку є виклик функції, то next виконає його як одну дію, ніби це чорна скринька, а step зайде всередину й покаже виконання функції по рядках.
Корисна мінітаблиця по кроках:
| Команда (ідея) | Зміст | Коли зручно |
|---|---|---|
|
бігти до наступної точки зупину | коли ви вже поставили «пастку» нижче |
|
крок на наступний рядок, не заходячи у виклики | коли викликана функція не викликає підозри |
|
крок із заходом усередину викликаної функції | коли підозрюєте, що всередині щось зламалося |
|
виконати поточну функцію до виходу | коли ви вже подивилися все потрібне й хочете «вистрибнути» назовні |
Тут є тонкість, яка потім рятує нерви: якщо ви випадково зайшли в стандартну бібліотеку й бачите нутрощі fmt або runtime, не панікуйте. Зазвичай достатньо finish, щоб повернутися у свою функцію, або поставити точку зупину у своєму коді й виконати continue.
Перегляд змінних і виразів
Сила налагоджувача в тому, що ви перестаєте вгадувати. У момент зупинки ви можете запитати: які значення в аргументів функції? Що лежить у slice? Який зараз len? Чи не nil вказівник? Який конкретно тип усередині interface{} або any? Це як рентген: неприємно лише вперше, а потім стає звичним робочим інструментом.
У Delve є команди, що дають змогу друкувати вирази й переглядати локальні змінні. В IDE зазвичай це вкладки Variables, Locals, Watches. У консольному стилі це схоже на print expr, locals, args. Важливо пам’ятати, що ви можете друкувати не лише змінну цілком, а й вирази: len(tasks), tasks[i].ID, u == nil тощо.
Дуже типова схема розслідування виглядає так: ви зупинилися на рядку перед підозрілою операцією, надрукували 2–3 значення, переконалися, що одне з них несподіване, наприклад індекс дорівнює len(slice), а не len(slice)-1, і у вас зʼявилася конкретна причина бага, а не містичне «ну воно іноді падає».
Чому кроки можуть «стрибати»: оптимізації та inlining
Коли ви налагоджуєте програму, то очікуєте, що виконання йтиме строго «за текстом». Але компілятор Go розумний і любить оптимізувати. Він може спростити вирази, переупорядкувати обчислення, викинути тимчасові змінні та вбудувати inline маленькі функції прямо в місце виклику. Для продуктивності це чудово. Для покрокового налагодження — іноді дивно: ви натискаєте крок, а курсор ніби «перестрибує» або «не заходить» туди, куди ви очікували.
Щоб налагодження було більш передбачуваним, часто використовують налагоджувальну збірку без оптимізацій і без inlining. У Delve це зазвичай роблять через прапорці збірки на кшталт -gcflags "all=-N -l". Ідея проста: -N зменшує оптимізації, -l вимикає inlining. Не обов’язково пам’ятати ці прапорці як закляття, достатньо пам’ятати зміст: якщо налагоджувач поводиться дивно, спробуйте простішу збірку.
Окремо зазначу: це не «магічний режим правильності». Він робить програму повільнішою й ближчою до вихідного тексту, але баги від цього не зникають — зникають лише сюрпризи в кроках налагоджувача.
3. Мінісценарій: ловимо баг із вказівниками
Давайте прив’яжемо Delve до чогось реального. Уявімо, що ми продовжуємо розвивати невеликий консольний застосунок для задач — умовний taskapp. У ньому є структура Task і функція, яка повертає список вказівників на задачі, бо ми хочемо змінювати їх через вказівники. Це типова ідея новачка — і іноді вона справді потрібна.
Нехай у нас є такий код: він компілюється, виглядає пристойно… і водночас містить класичну пастку повторного використання змінної:
package taskapp
type Task struct {
ID int
Text string
}
func ToPointers(tasks []Task) []*Task {
var res []*Task
var t Task
for _, x := range tasks {
t = x
res = append(res, &t)
}
return res
}
Якщо ви викличете ToPointers([]Task{{1,"a"},{2,"b"}}), то очікуватимете два різні вказівники на дві різні задачі. А на практиці часто отримаєте «два вказівники на одне й те саме місце», бо змінна t одна й та сама, і ви щоразу додаєте адресу тієї самої змінної. У результаті всі елементи res вказують на один обʼєкт — на останнє присвоєне значення t.
Це чудовий кандидат для налагодження в Delve, бо очима в коді легко щось не помітити, а в налагоджувачі можна побачити адреси й значення на кожному кроці.
Щоб нам було зручніше спостерігати, додамо невеликий приклад використання в окремому пакеті main, щоб його можна було запускати:
package main
import (
"fmt"
"example/taskapp"
)
func main() {
tasks := []taskapp.Task{{ID: 1, Text: "a"}, {ID: 2, Text: "b"}}
ptrs := taskapp.ToPointers(tasks)
fmt.Println(ptrs[0].ID, ptrs[1].ID) // очікуємо: 1 2 (а іноді побачимо: 2 2)
}
Тепер сценарій налагодження. Нам потрібно зупинитися всередині ToPointers і подивитися, яку адресу ми додаємо в res на кожній ітерації та що лежить за цією адресою.
Логіка дій у Delve зазвичай така:
1) Поставити точку зупину на taskapp.ToPointers
2) Запустити continue, щоб зупинитися у функції
3) Крокувати по циклу і дивитися:
- значення t
- адресу &t
- що потрапило в res
Якщо показати це «уривком сесії» — дуже спрощено, без претензії на ідеальну копію виводу, — то думки й команди будуть приблизно такими:
(dlv) break taskapp.ToPointers
(dlv) continue
(dlv) next
(dlv) print &t
(dlv) next
(dlv) print &t
Ключова перевірка тут — саме &t. Якщо ви бачите, що адреса однакова на обох ітераціях, пазл складається: ви додаєте один і той самий вказівник двічі.
Після цього виправлення зазвичай просте: потрібно брати адресу елемента слайса, а не тимчасової змінної. Наприклад, через індексний цикл:
package taskapp
func ToPointers(tasks []Task) []*Task {
res := make([]*Task, 0, len(tasks))
for i := range tasks {
res = append(res, &tasks[i])
}
return res
}
І ось тут Delve корисний не лише для того, щоб знайти проблему, а й для того, щоб переконатися: виправлення справді спрацювало. Після правки ви знову ставите точку зупину, дивитеся &tasks[i] на кожній ітерації й бачите різні адреси.
4. Як мислити під час налагодження з Delve
Щоб не перетворювати налагодження на хаотичне «тик-тик-тик по кнопках», корисно мати один стійкий цикл дій. Він дуже схожий на те, що ми робили з print-debugging, тільки тепер інструменти багатші.
flowchart TD
A[Є симптом: panic / неправильний результат] --> B[Гіпотеза: де очікування розходяться з фактом]
B --> C[Ставимо точку зупину біля місця проблеми]
C --> D[Зупиняємося і дивимося 2–3 ключові значення]
D --> E{Гіпотезу підтверджено?}
E -- так --> F[Виправляємо код і перевіряємо ще раз]
E -- ні --> G[Уточнюємо гіпотезу й переносимо точку зупину]
G --> C
Якщо ви триматимете в голові цю схему, Delve перестане бути «складним інструментом для дорослих» і стане просто прискорювачем перевірки гіпотез.
5. Типові помилки під час роботи з Delve
Помилка № 1: намагатися налагоджувати без гіпотези, просто «дивитися змінні».
Це найчастіший шлях до втоми. Ви зупиняєтеся, бачите десятки значень і не розумієте, що з цього важливо. Набагато продуктивніше заздалегідь сформулювати коротке запитання: «чому id став 0?», «чому len(tasks) дорівнює 0?», «чому cfg == nil?». Тоді й дивитися ви будете лише 2–3 значення, а не весь світ.
Помилка № 2: ставити точку зупину занадто рано й отримувати тисячу зупинок.
Якщо поставити точку зупину «на початку програми» і далі крокувати, можна постаріти ще до того, як ви дійдете до бага. Зазвичай корисніше ставити точку зупину ближче до місця, де порушується інваріант: перед небезпечним рядком або перед гілкою, яка веде до помилки. Чим точніша точка зупину, тим менше зайвого шуму.
Помилка № 3: застрягти в стандартній бібліотеці й думати, що це тупик.
step по виклику fmt.Println може завести вас у хащі форматування рядків, і новачок там іноді «залишається жити». Якщо ви бачите, що опинилися не у своєму коді, це не провал. Зазвичай достатньо зробити finish, щоб вийти з поточної функції, або поставити точку зупину у своєму файлі й натиснути continue.
Помилка № 4: дивуватися «дивним крокам» через оптимізації та inlining.
Іноді здається, що налагоджувач «бреше»: рядок пропущено, змінна «не існує», стрибки не збігаються з очікуваннями. Часто це не баг Delve, а наслідок оптимізацій компілятора. У таких випадках допомагає налагоджувальна збірка з вимкненням оптимізацій і inlining — ідея прапорців -N -l. Якщо ви бачите дивну поведінку, не робіть висновок «я не вмію налагоджувати» — спочатку перевірте режим збірки.
Помилка № 5: намагатися «полагодити через налагоджувач», змінюючи значення абияк.
Деякі налагоджувачі дають змогу змінювати значення змінних «на льоту». Це може бути корисно, але для новачка часто перетворюється на самообман: «я змінив змінну, і воно запрацювало». Так, але у вихідному коді ви нічого не виправили. Використовуйте налагоджувач, щоб зрозуміти причину, а виправлення робіть у вихідному коді й перевіряйте все повторним запуском.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ