Отладка не должна быть частью разработки, потому что она непродуктивна и отнимает много времени. В идеале код нужно сразу делать чистым, понятным и покрывать тестами. Но хотя современные подходы к разработке ПО не подразумевают дальнейшей отладки, мы каждый день продолжаем сталкиваться с унаследованным кодом, который может быть не покрыт тестами, быть сложным и запутанным. И в результате нам всё же приходится заниматься этим неблагодарным делом.
Сегодня есть множество IDE, поддерживающих работу с Go и позволяющих отлаживать приложения. На текущий момент для Go представлены два отладчика: GDB (но он не поддерживает многие фичи языка, например Go-рутины) и Delve. Многие IDE используют последний как дефолтный отладчик. И в этой статье я расскажу о возможностях Delve: о том, что умеет сам отладчик, а не что нам предоставляет IDE.
Основы работы с Delve
Для того чтобы начать работу с отладчиком, нужно скомпилировать программу на Go и выполнить в командной строке команду dlv debug, находясь в директории с исполняемым файлом. После этого мы попадём в Delve. Для начала работы требуется установить первую точку останова и выполнить команду continue.
Рассмотрим пример.
Возьмём простую программу на Go, которая читает данные из текстового файла и обновляет его, если объём данных не превышает 12 байт. А если объём равен 12 байтам, то программа просто выводит строку hello и завершает выполнение.
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
)
func main() {
file, err := os.Open("test.txt")
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Errorf(" problem: %v", err)
}
fmt.Println(data)
fmt.Println(len(data))
if len(data) == 12 {
fmt.Println("hello")
return
}
data = append(data, byte(len(data)))
err = ioutil.WriteFile("test.txt", data, 0644)
if err != nil {
log.Fatal(err)
}
}
Так выглядит моя директория перед компиляцией:
Теперь скомпилируем программу, выполнив команду go build main.go в командной строке. В результате должно получиться вот что:
Получив бинарный файл, заходим в директорию с ним и выполняем команду dlv debug:
Далее устанавливаем в файле точку останова на строке номер 14, выполнив команду break main.go:14:
И запускаем отладку с помощью команды continue:
Исполнение программы остановилось на 14-й строке. Теперь можно посмотреть значения переменных:
Чтобы продолжить отладку, нужно в командной строке либо выполнить команду next (и тогда выполнится следующая строка кода), либо набрать continue, (и программа выполнится до следующей точки останова).
Теперь вкратце расскажу про основные команды Delve, с помощью которых вы сможете отлаживать свои приложения:
next — следующая строка;
-
step — вход внутрь вызываемой функции:
-
continue — следующая точка останова (breakpoint):
break — установка точки останова, например break m67 main.go:67;
cond — задаёт условия, при которых произойдёт останова на текущей команде отладки. Например, при выполнении команды cond m67 len(array) == 8 сработает останова на этой строке, если в массиве будет восемь элементов;
breakpoints — отображает все заданные точки останова;
print — распечатывает значение выражения или переменной;
-
vars— выводит значения всех загруженных переменных приложения:
-
locals — выводит значения локальных переменных функции:
Это основные команды Delve, которых будет достаточно для начала работы с отладчиком. Разумеется, инструментарий решения гораздо серьёзнее, и подробнее обо всех командах вы можете узнать из официальной документации.
Но главной фишкой Delve является возможность создавать пользовательские команды, которые позволяют гибче использовать отладчик и открывают широкие возможности для автоматизации. Давайте рассмотрим синтаксис и пример создания пользовательской команды.
Пишем свои команды на Starlark
Delve поддерживает синтаксис Starlark — это диалект Python, который позволяет писать полезные и функциональные плагины. Так как Starlark был придуман для написания небольших программ-конфигураций в отладчиках, а не программ, которые будут долго выполняться, он не содержит таких возможностей Python, как классы, исключения и рефлексия.
На Starlark, например, можно написать команду для создания дампа текущего приложения и перезапуска его отладки уже с новыми дампом и данными. Такая функциональность может пригодиться, если какая-то ошибка воспроизводится только в очень «экзотических» случаях.
Структура программ-конфигураций на языке Starlark:
def command_название команды
"Комментарий, который будет выведен, если набрать help имя команды"
Далее пишем код.
Синтаксис языка можно посмотреть здесь.
Давайте рассмотрим пример создания команды для Delve:
def command_flaky(args):
"Repeatedly runs program until a breakpoint is hit"
while True:
if dlv_command("continue") == None:
break
dlv_command("restart")
Эта команда будет перезапускать отладку до тех пор, пока не будет достигнута точка останова. Чтобы выполнить её в Delve:
Сохраните команду в файл с расширением .star.
Запустите Delve.
Выполните в командной строке команду source flaky.star.
Расставьте точки останова.
Выполните команду flaky.
Для работы с flaky возьмём программу из предыдущего раздела. Пример того, что отобразится в консоли отладчика:
Как видите, программа была перезапущена семь раз, и при каждом выполнении условия срабатывала точка останова. Отлавливать такие вещи вручную в Visual Studio Code и других средах разработки не так-то просто.
Если вам интересно, что ещё можно сделать в Delve с помощью Starlark-синтаксиса, за подробностями добро пожаловать сюда. А если вы не любите использовать командную строку или не хотите разбираться в тонкостях «неродного» языка, то давайте рассмотрим, как сделать то же самое на Go.
Написание плагинов на Go
Рассмотрим этот процесс на примере удалённой отладки приложений. В Delve реализован gRPC-сервер, к которому можно обращаться по API. Для этого сначала необходимо установить Delve рядом с приложением. Если вы используете микросервисную архитектуру, то можно добавить этот инструмент в образ вашего контейнера.
Возьмём код из первого раздела и попробуем отладить его с помощью Go. Для этого нам нужно выполнить в командной строке команду:
dlv exec --continue --headless --accept-multiclient --api-version 2 --listen 0.0.0.0:50080 main
Открываем любимую IDE и пишем на Go:
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/go-delve/delve/service/api"
"github.com/go-delve/delve/service/rpc2"
)
func main() {
serverAddr := "localhost:50080"
funcToTrace := "main.main"
// Create a new connection to the Delve debug server.
// rpc2.NewClient will log.Fatal if connection fails so there
// won't be an error to handle here.
client := rpc2.NewClient(serverAddr)
defer client.Disconnect(true)
// Stop the program we are debugging.
// The act of halting the program will return it's current state.
state, err := client.Halt()
if err != nil {
bail(err)
}
bp := &api.Breakpoint{
FunctionName: funcToTrace,
Tracepoint: true,
Line: 12,
}
client.Restart(false)
tracepoint, err := client.CreateBreakpoint(bp)
if err != nil {
bail(err)
}
defer client.ClearBreakpoint(tracepoint.ID)
for _, i := range []int{1, 2} {
fmt.Printf("i:\t %d\n", i)
client.Restart(false)
// Continue the program.
stateChan := client.Continue()
// Create JSON encoder to write to stdout.
enc := json.NewEncoder(os.Stdout)
fmt.Println("____________________________________________")
fmt.Println("state")
for state = range stateChan {
// Write state to stdout.
enc.Encode(state)
}
fmt.Println("____________________________________________")
}
}
func bail(s interface{}) {
fmt.Println(s)
os.Exit(1)
}
Что происходит на стороне сервера, когда идёт отладка:
Тут видно, что было несколько перезапусков приложения. На стороне же приложения будет следующий вывод:
Изучим информацию, которую выдаёт Delve:
Pid — идентификатор приложения в Linux;
Running — запущено ли приложение;
Recording — идёт запись информации о процессе;
CoreDumping — идёт запись дампа приложения;
Threads — информация о потоках исполнения приложения;
breakPoint — информация о сработавшей точке останова.
Подробно про выведенную информацию можно почитать здесь.
Отладка приложения с помощью написания другого приложения позволяет создавать анализаторы поведения программы и автоматизировать проверку своих приложений. Если вам захотелось написать что-то такое, то вам поможет gRPC-клиент.
Заключение
Я только поверхностно ознакомил вас с возможностями Delve. Показал, что мы можем отлаживать код и без IDE. Можно писать анализаторы поведения программ и приложения для отладки своего приложения. Наконец, функциональность Delve можно расширять собственными командами, что делает его очень мощным инструментом.