Дебажим Golang с помощью Delve
- пятница, 15 сентября 2023 г. в 00:00:23
В этой статье мы рассмотрим, как дебажить программы, написанные на Golang, с помощью Delve. Delve — это сторонний отладчик для Go, скачать на github по ссылке. Это хорошая альтернатива отладчику GDB golang, так как Delve куда больше возможностей для работы.
Delve лучше, чем GDB, понимает среду выполнения Go, структуры данных и выражения. В настоящее время Delve поддерживает Linux, OSX и Windows в версии amd64. Наиболее актуальный список поддерживаемых платформ приведен в документации Delve.
Вы узнаете, как просмотривать, добавлять и изменять точки останова в Go, перемещаться по программе построчно или через точки останова, проверять значения переменных, функций и выражений.
Go Delve можно загрузить и установить с помощью команды go get
.
$ go get github.com/go-delve/delve/cmd/dlv
Если вы используете модули Go, то, возможно, захотите выполнить эту команду вне каталога проекта, чтобы не добавлять delve в качестве зависимости в файл go.mod.
Если у вас есть установленный и работающий Go, то все нижеперечисленное уже должно быть настроено
Убедитесь, что правильно установлена переменная env GOBIN, которая будет указывать на каталог, в котором будет храниться команда dlv (delve). Проверить это можно, набрав go env GOBIN
Также убедитесь, что PATH содержит GOBIN, что позволит запускать бинарные исполняемые файлы Go без указания абсолютного пути.
В mac OS также может потребоваться включить инструменты разработчика, набрав следующую команду
xcode-select --install
После завершения установки вы можете убедиться в том, что delve успешно установлена.
Проверьте её версию.
$ dlv version
Delve Debugger
Version: 1.5.1
Build: $Id: bca418ea7ae2a4dcda985e623625da727d4525b5 $
Вы успешно установили delve. Теперь приступим к отладке.
Для запуска сеанса отладки можно воспользоваться одной из команд, доступных из командной строки dlv help
$ dlv help
...
dlv [command]
Available Commands:
attach Attach to running process and begin debugging.
connect Connect to a headless debug server.
core Examine a core dump.
dap [EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
debug Compile and begin debugging main package in current directory, or the package specified.
exec Execute a precompiled binary, and begin a debug session.
help Help about any command
run Deprecated command. Use 'debug' instead.
test Compile test binary and begin debugging program.
trace Compile and begin tracing program.
version Prints version.
Интересными для нас являются команды dlv debug и dlv exec, которые используются для запуска сеанса отладки. Разница в том, что одна из них (dlv debug) выполняет компиляцию бинарного файла из исходного текста, а другая (dlv exec) требует наличия скомпилированного бинарного файла.
Команда test
также полезна, если мы хотим отладить тест Go.
Приведенный ниже фрагмент кода представляет собой пример кода, который мы будем использовать для отладки, — это реализация числа Фибоначчи.
package main
import "fmt"
var m = make(map[int]int, 0)
func main() {for _, n := range []int{5, 1, 9, 98, 6} {x := fib(n)fmt.Println(n, "fib", x)}}
func fib(n int) int {if n < 2 {return n}
var f int
if v, ok := m[n]; ok {
f = v
} else {
f = fib(n-2) + fib(n-1)
m[n] = f
}
return f
}
Вы можете проверить и запустить этот фрагмент кода на Go playground.
Нам необходимо передать основной пакет, который будет компилироваться и выполняться для отладки.
dlv debug main.go
Введите 'help' для получения списка команд.
По итогу запустится сеанс отладки.
После того как наша отладочная сессия началась, мы скомпилировали и присоединили к двоичному файлу Go, мы можем приступить к отладке нашей программы. Перед нами открывается новый repl, который представляет собой интерпретатор delve, который также можно назвать «клиентом delve». Он будет посылать отладочные команды на наш ранее созданный сервер delve.
Запуск программы:
call
------------------------ Возобновляет процесс, внедряя вызов функции
continue (alias: c)
--------- Выполняется до точки останова или завершения программы.
next (alias: n)
------------- Переход к следующей строке исходного текста.
rebuild
--------------------- Пересобирает целевой исполняемый файл и перезапускает его. Не работает, если исполняемый файл не был собран программой delve.
restart (alias: r)
---------- Перезапуск процесса.
step (alias: s)
------------- Одиночный шаг по программе.
step-instruction (alias: si)
Однократный шаг по одной инструкции процессора.
stepout (alias: so)
--------- Выход из текущей функции.
Манипулирование точками останова:
break (alias: b)
------- Устанавливает точку останова.
breakpoints (alias: bp)
Выводит информацию об активных точках останова.
clear
------------------ Удаляет точку останова.
clearall
--------------- Удаляет несколько точек останова.
condition (alias: cond)
Установка условия точки останова.
on
--------------------- Выполняет команду при достижении точки останова.
trace (alias: t)
------- Устанавливает точку трассировки.
Просмотр переменных и памяти программы:
args
----------------- Печать аргументов функции.
display
-------------- Выводить значение выражения при каждой остановке программы.
examinemem (alias: x)
Исследование памяти:
locals
--------------- Вывести локальные переменные.
print (alias: p)
----- Оценить выражение.
regs
----------------- Вывести содержимое регистров процессора.
set
------------------ Изменение значения переменной.
vars
----------------- Вывести переменные пакета.
whatis
--------------- Выводит тип выражения.
Вывод списка и переключение между потоками и горутинами:
goroutine (alias: gr)
-- Показывает или изменяет текущую горутину
goroutines (alias: grs)
Список программных горутин.
thread (alias: tr)
----- Переход к указанному потоку.
threads
---------------- Вывод информации для каждого отслеживаемого потока.
Просмотр стека вызовов и выбор фреймов:
deferred
--------- Выполнение команды в контексте отложенного вызова.
down
------------- Переместить текущий фрейм вниз.
frame
------------ Установить текущий фрейм или выполнить команду на другом фрейме.
stack (alias: bt)
Вывод трассировки стека.
up
--------------- Переместить текущий фрейм вверх.
Другие команды:
config
--------------------- Изменение параметров конфигурации.
edit (alias:)
----------- Открывает место, где вы находитесь в $DELVE_EDITOR или $EDITOR.
exit (alias: quit | q)
----- Выход из отладчика.
funcs
---------------------- Вывести список функций.
help (alias: h)
------------ Печать справочного сообщения.
libraries
------------------ Список загруженных динамических библиотек
list (alias: ls | l)
------- Показать исходный код.
source
--------------------- Выполняет файл, содержащий список команд delve.
sources
-------------------- Вывести список исходных файлов.
types
---------------------- Вывести список типов
Для получения полной документации введите help
, а затем команду.
Первая команда, на которую я хотел бы обратить ваше внимание, - это команда list, позволяющая вывести список исходных текстов в заданном месте. Мы можем указать местоположение, передав имя пакета и функцию или путь к файлу и строку. Например
Показывает исходный код по пакету и имени функции.
(dlv) list main.main
Showing /workspace/tutorials/delve/main.go:7 (PC: 0x10d145b)
2:
3: import "fmt"
4:
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
(dlv)
Показывает исходный код по имени файла и номеру строки
(dlv) list ./main.go:14
Showing /workspace/tutorials/delve/main.go:14 (PC: 0x10d1713)
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
16: return n
17: }
18:
19: var f int
(dlv)м
Также существует команда для поиска функций по заданному шаблону.
(dlv) funcs fib
main.fib
Если вы застряли в сеансе отладки, вы можете выйти из него:
(dlv) exit
После того как вы узнали, как вывести на экран участок исходного кода с помощью команды delve list, вы можете начать добавлять точки останова в свою программу в тех местах, где вы хотите остановиться и проверить значения переменных и других выражений.
Для данного примера предположим, что мы хотим добавить точку останова в строке 10 файла main.go, который мы уже видели в предыдущем примере listexample. Для этого необходимо использовать ключевое слово break, за которым следует место, куда нужно добавить точку останова.
Это добавит точку останова в указанное место, а также перечислит, где эта точка будет использоваться в строке 10.
(dlv) break ./main.go:10
Breakpoint 1 set at 0x10d155d for main.main() ./main.go:10
(dlv) list ./main.go:10
Showing /workspace/tutorials/delve/main.go:10 (PC: 0x10d155d)
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
(dlv)
Чтобы перечислить все текущие точки останова для данного сеанса отладки, используйте:
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x10388c0 for runtime.fatalthrow() /usr/local/go/src/runtime/panic.go:1162 (0)
Breakpoint unrecovered-panic at 0x1038940 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1189 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x10d155d for main.main() ./main.go:10 (0)
В данном примере мы видим 3 точки останова. Первые две автоматически добавляются программой delve и используются для защиты от фатальных ошибок, чтобы мы могли точно определить состояние нашей программы и просмотреть переменные, трассировку стека и состояние.
Третья точка останова с надписью Breakpoint 1 — это та, которую мы добавили в строке 10.
Для удаления конкретной точки останова из сеанса отладки:
(dlv) clear 1
Breakpoint 1 cleared at 0x10d155d for main.main() ./main.go:10
Это удобно, если необходимо удалить конкретную точку останова, добавленную по ошибке, или просто потому, что нужно удалить и начать отладку другого участка той же программы.
Комнада, чтобы очистить все добавленные вручную точки останова:
(dlv) break ./main.go:8
Breakpoint 1 set at 0x10d1472 for main.main() ./main.go:8
(dlv) break ./main.go:9
Breakpoint 2 set at 0x10d154a for main.main() ./main.go:9
(dlv) break ./main.go:10
Breakpoint 3 set at 0x10d155d for main.main() ./main.go:10
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x10388c0 for runtime.fatalthrow() /usr/local/go/src/runtime/panic.go:1162 (0)
Breakpoint unrecovered-panic at 0x1038940 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1189 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x10d1472 for main.main() ./main.go:8 (0)
Breakpoint 2 at 0x10d154a for main.main() ./main.go:9 (0)
Breakpoint 3 at 0x10d155d for main.main() ./main.go:10 (0)
(dlv) clearall
Breakpoint 1 cleared at 0x10d1472 for main.main() ./main.go:8
Breakpoint 2 cleared at 0x10d154a for main.main() ./main.go:9
Breakpoint 3 cleared at 0x10d155d for main.main() ./main.go:10
В приведенном примере мы создали 3 точки останова, в строках 8, 9 и 10. Мы отображаем все точки останова, а затем сразу же очищаем их. Это может быть очень удобно, когда необходимо очистить все точки останова и перейти к отладке другого участка той же программы.
После того как мы установили все точки останова и можем просматривать любую часть нашего исходного кода с помощью списка, мы можем посмотреть, как можно «отлаживать» и запускать нашу программу в режиме отладки с помощью набора очень мощных команд.
Выполняет программу до следующей точки останова или до завершения работы программы:
(dlv) break ./main.go:10
Breakpoint 1 set at 0x10d155d for main.main() ./main.go:10
(dlv) continue
main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10d155d)
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
=> 10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
Установив точку останова на строке 10 в файле main.go, мы можем просто выполнить команду continue, и наш отладчик будет выполнять программу до следующей точки останова, которой в нашем случае является точка останова 1 на строке 10. В этот момент мы можем делать довольно много вещей, например, осматривать и изменять значение переменных.
Переход к следующей заданной строке:
(dlv) next
5 fib 5
main.main() ./main.go:8 (PC: 0x10d1693)
3: import "fmt"
4:
5: var m = make(map[int]int, 0)
6:
7: func main() {
=> 8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
Команда next просто позволяет нам двигаться вперед по одной инструкции за раз, как указано в исходном коде, независимо от наличия или отсутствия точек останова. Это очень удобно, если необходимо проанализировать программу.
Используется, чтобы дать указание отладчику перейти внутрь вызова функции.
(dlv) next
main.main() ./main.go:9 (PC: 0x10d154a)
4:
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
=> 9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
(dlv) step
main.fib() ./main.go:14 (PC: 0x10d1713)
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
=> 14: func fib(n int) int {
15: if n < 2 {
16: return n
17: }
18:
19: var f int
С помощью step мы можем перейти внутрь определения функции, а не просто вычислить ее значение и двигаться дальше. Это очень полезно при отслеживании логики нескольких вызовов функций, возвращающих результаты, природу и происхождение которых мы хотим исследовать. При использовании step в строках, не являющихся вызовами функций, он будет вести себя так же, как команда next delve.
Команда возвращает нас к вызывающей функции, в которой мы находимся.
(dlv) stepout
main.main() ./main.go:9 (PC: 0x10d1553)
Values returned:
~r1: 1
4:
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
=> 9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
Restart позволит нам перезапустить программу в случае, если она завершится, а мы все еще хотим вести отладку. Это особенно полезно, если мы не хотим потерять все точки останова и не хотим выходить из программы и создавать новый отладочный сервер delve с нуля.
(dlv) clearall
Breakpoint 4 cleared at 0x10d155d for main.main() ./main.go:10
(dlv) continue
1 fib 1
9 fib 34
98 fib 6174643828739884737
6 fib 8
Process 39014 has exited with status 0
(dlv) restart
Process restarted with PID 39050
В нашем примере просто убираем все точки останова, продолжаем работу, чтобы программа выполнилась до конца, и снова перезапускаем процесс.
Print позволяет нам видеть содержимое переменных и оценивать выражения:
(dlv) break ./main.go:10
Breakpoint 1 set at 0x10d155d for main.main() ./main.go:10
(dlv) continue
main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10d155d)
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
=> 10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
(dlv) print x
5
В приведенном примере мы установили точку останова в строке 10 файла main.go и вывели значение переменной x, которое является значением фибоначчи для последовательности в позиции 5, как обозначено в приведенном выше коде.
Теперь можно попробовать перейти внутрь функции fib и попытаться вывести различные значения, например n или переменную карты m!
Команда locals может пригодиться для исследования содержимого всех локальных переменных.
(dlv) list
main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10d155d)
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
=> 10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
(dlv) locals
n = 5
x = 5
Мы запустили курс по Go для инженеров. Поток уже идет, но вы можете к нему присоединиться. Узнать программу подробнее и оставить заявку вы можете на нашем сайте.