1. Когда и зачем нужен Delve
Отладчик часто воспринимают как волшебную кнопку «сделай, чтобы работало». Увы, он не чинит код, он всего лишь позволяет вам наблюдать его исполнение в замедленной съёмке. Но в этой «замедленке» есть огромная сила: можно остановиться ровно в нужной строке, посмотреть реальные значения переменных и понять, на каком шаге ваши ожидания разошлись с реальностью.
Delve (команда dlv) — стандартный отладчик для Go. Он полезен, когда вы уже примерно знаете, где проблема (например, по stack trace), но пока не понимаете, почему значения стали такими. При этом Delve не всегда доступен (ограничения окружения, политика компании, «у меня Windows и сегодня всё против меня»), поэтому навыки чтения stack trace и диагностического вывода всё равно должны быть у вас «на руках».
Простая модель работы
Чтобы 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: пытаться “починить через отладчик”, меняя значения как попало.
Некоторые отладчики позволяют модифицировать значения переменных на лету. Это может быть полезно, но для новичка часто превращается в самообман: «я поменял переменную, и оно заработало». Да, но в коде-то вы ничего не исправили. Используйте отладчик, чтобы понять причину, а исправление делайте в исходниках и перепроверяйте повторным запуском.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ