Отладка кода: delve, VS Code и грязный print
Существует одна стыдная истина: подавляющее большинство Go-программистов
отлаживается fmt.Println. Это работает, но в 4 случаях из 5 это
дольше и грязнее, чем поставить breakpoint и посмотреть состояние
напрямую. На junior-уровне нормально начинать с print-debug, но к
концу этого урока у тебя на ноуте должен быть dlv, на котором ты
прошёл одну реальную сессию.
Дальше в 06-functions будут функции, и дебаггер пригодится: войти в функцию по step-in проще, чем читать стек.
1. Три уровня отладки
| Уровень | Инструмент | Когда |
|---|---|---|
| Грубый | fmt.Println, log.Printf, log/slog | быстро глянуть значение раз |
| Структурный | log/slog с уровнями и атрибутами | в боевом коде, чтобы не выпиливать перед коммитом |
| Полный | dlv (delve) или дебаггер в IDE | сложный баг, состояние с десятком переменных, паника в чужом коде |
Идея простая: чем серьёзнее баг, тем выше по уровням лезешь. И обратно:
если три Println решают вопрос, не надо ставить дебаггер.
2. Грязный print, по-нормальному
fmt.Printf("DBG: user=%+v err=%v\n", user, err)
Три быстрых правила:
- Префикс
DBG:или похожий, чтобы потом найти и удалить grep-ом. %+vдля структур, иначе видишь{1 hi}без названий полей.- Печатать в
os.Stderrчерезfmt.Fprintln(os.Stderr, ...), если вывод программы (stdout) важен и не должен мусориться. Например, для CLI которая читается пайпом.
Это нормально для разовых проверок. Но как только баг повторяется чаще одного раза, пора в дебаггер.
log/slog когда не разовое
Когда логи нужны не на 5 минут, а на месяцы (в проде или в CI),
бери log/slog, он в stdlib с Go 1.21:
import "log/slog"
slog.Info("user fetched", "id", user.ID, "took", elapsed)
slog.Error("fetch failed", "url", url, "err", err)
Преимущество: можно настроить JSON-вывод одной строкой, поставить
уровень debug, и забыть. Подробнее в
15-idioms-and-stdlib §5.
3. Установка delve
Delve
это дебаггер уровня gdb, но заточенный под Go. Без него VS Code и
GoLand не умеют отлаживать.
go install github.com/go-delve/delve/cmd/dlv@latest
dlv version
# Delve Debugger
# Version: 1.24.x
Проверь, что бинарь попал в $PATH:
which dlv
# /Users/you/go/bin/dlv
Если “command not found”, допиши $(go env GOPATH)/bin в $PATH (см.
урок 00).
На macOS первый запуск может попросить подписать бинарь. Это нормально:
codesign -s - -f --entitlements <(cat <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.cs.debugger</key><true/>
</dict></plist>
EOF
) $(which dlv)
VS Code и GoLand сделают это сами через свои Go-плагины.
4. Пять команд delve, которых хватит на 95%
Запуск:
dlv debug # текущий main-пакет
dlv debug ./cmd/avg # конкретный
dlv test ./internal/strbench # отлаживать тесты
dlv attach <pid> # подключиться к запущенному процессу
Ты попадаешь в REPL (dlv). Команды (полное имя и сокращение):
| Команда | Сокращение | Что делает |
|---|---|---|
break <file>:<line> | b | поставить breakpoint |
continue | c | бежать до следующего breakpoint |
next | n | следующая строка, не заходя в функции |
step | s | следующая строка, заходя в функцию |
print <expr> | p | напечатать значение выражения |
locals | lo | все локальные переменные |
args | аргументы текущей функции | |
goroutines | grs | список горутин |
goroutine <id> | gr | переключиться на горутину |
stack | bt | стек вызовов |
restart | r | перезапустить программу |
quit | q | выйти |
Минимальная сессия:
$ dlv debug ./cmd/avg
Type 'help' for list of commands.
(dlv) break main.main
Breakpoint 1 set at 0x... for main.main()
(dlv) continue
> main.main() ./main.go:12 (hits goroutine(1):1 total:1)
(dlv) next
> main.main() ./main.go:13
(dlv) print sum
0
(dlv) locals
sum = 0
count = 0
scanner = bufio.Scanner{...}
(dlv) continue
То есть: поставил breakpoint на функцию, добежал, прошёл одну строку, посмотрел переменные. Этого хватит на огромное количество багов.
5. Условные breakpoint и watchpoint
Иногда строка выполняется тысячу раз, и интересна одна конкретная. Условный breakpoint:
(dlv) break main.go:42 cond i == 999
Прерывание сработает только когда i == 999. Удобно, когда баг
вылезает на тысячной итерации.
watch <expr> останавливает выполнение, когда значение выражения
меняется. Полезно ловить, когда какое-то поле затирают:
(dlv) watch -w user.Active
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.