golang

Дебажим Golang с помощью Delve

  • пятница, 15 сентября 2023 г. в 00:00:23
https://habr.com/ru/companies/southbridge/articles/761016/

В этой статье мы рассмотрим, как дебажить программы, написанные на Golang, с помощью Delve. Delve — это сторонний отладчик для Go, скачать на github по ссылке. Это хорошая альтернатива отладчику GDB golang, так как Delve куда больше возможностей для работы.

Delve лучше, чем GDB, понимает среду выполнения Go, структуры данных и выражения. В настоящее время Delve поддерживает Linux, OSX и Windows в версии amd64. Наиболее актуальный список поддерживаемых платформ приведен в документации Delve.

Вы узнаете, как просмотривать, добавлять и изменять точки останова в Go, перемещаться по программе построчно или через точки останова, проверять значения переменных, функций и выражений.

Загрузка и установка Go Delve

Go Delve можно загрузить и установить с помощью команды go get.

Linux, Windows, OSX

$ go get github.com/go-delve/delve/cmd/dlv

Если вы используете модули Go, то, возможно, захотите выполнить эту команду вне каталога проекта, чтобы не добавлять delve в качестве зависимости в файл go.mod.

Если у вас есть установленный и работающий Go, то все нижеперечисленное уже должно быть настроено

Убедитесь, что правильно установлена переменная env GOBIN, которая будет указывать на каталог, в котором будет храниться команда dlv (delve). Проверить это можно, набрав go env GOBIN

Также убедитесь, что PATH содержит GOBIN, что позволит запускать бинарные исполняемые файлы Go без указания абсолютного пути.

OSX

В 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.

Пример кода Golang, который необходимо отладить

Приведенный ниже фрагмент кода представляет собой пример кода, который мы будем использовать для отладки, — это реализация числа Фибоначчи.

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' для получения списка команд.

По итогу запустится сеанс отладки.

Клиент Delve

После того как наша отладочная сессия началась, мы скомпилировали и присоединили к двоичному файлу 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, а затем команду.

Общие команды Delve

Первая команда, на которую я хотел бы обратить ваше внимание, - это команда list, позволяющая вывести список исходных текстов в заданном месте. Мы можем указать местоположение, передав имя пакета и функцию или путь к файлу и строку. Например

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)

List

Показывает исходный код по имени файла и номеру строки

(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)м

Также существует  команда для поиска функций по заданному шаблону.

Funcs

(dlv) funcs fib
main.fib

Exit

Если вы застряли в сеансе отладки, вы можете выйти из него:

(dlv) exit

Добавление точек останова с помощью Delve

После того как вы узнали, как вывести на экран участок исходного кода с помощью команды delve list, вы можете начать добавлять точки останова в свою программу в тех местах, где вы хотите остановиться и проверить значения переменных и других выражений.

Для данного примера предположим, что мы хотим добавить точку останова в строке 10 файла main.go, который мы уже видели в предыдущем примере listexample. Для этого необходимо использовать ключевое слово break, за которым следует место, куда нужно добавить точку останова.

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.

clear

Для удаления конкретной точки останова из сеанса отладки:

(dlv) clear 1

Breakpoint 1 cleared at 0x10d155d for main.main() ./main.go:10

Это удобно, если необходимо удалить конкретную точку останова, добавленную по ошибке, или просто потому, что нужно удалить и начать отладку другого участка той же программы.

clearall

Комнада, чтобы очистить все добавленные вручную точки останова:

(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. Мы отображаем все точки останова, а затем сразу же очищаем их. Это может быть очень удобно, когда необходимо очистить все точки останова и перейти к отладке другого участка той же программы.

Запуск и навигация по программе с помощью Delve

После того как мы установили все точки останова и можем просматривать любую часть нашего исходного кода с помощью списка, мы можем посмотреть, как можно «отлаживать» и запускать нашу программу в режиме отладки с помощью набора очень мощных команд.

continue

Выполняет программу до следующей точки останова или до завершения работы программы:

(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. В этот момент мы можем делать довольно много вещей, например, осматривать и изменять значение переменных. 

next

Переход к следующей заданной строке:

(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 просто позволяет нам двигаться вперед по одной инструкции за раз, как указано в исходном коде, независимо от наличия или отсутствия точек останова. Это очень удобно, если необходимо проанализировать программу.

step

Используется, чтобы дать указание отладчику перейти внутрь вызова функции. 

(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.

stepout

Команда возвращает нас к вызывающей функции, в которой мы находимся.

(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

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

В нашем примере просто убираем все точки останова, продолжаем работу, чтобы программа выполнилась до конца, и снова перезапускаем процесс. 

Как просматривать переменные программы с помощью Delve

print

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

Команда 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 для инженеров. Поток уже идет, но вы можете к нему присоединиться. Узнать программу подробнее и оставить заявку вы можете на нашем сайте.