Рассматриваем под лупой отладчик Delve для Go-разработчиков

Отладка не должна быть частью разработки, потому что она непродуктивна и отнимает много времени. В идеале код нужно сразу делать чистым, понятным и покрывать тестами. Но хотя современные подходы к разработке ПО не подразумевают дальнейшей отладки, мы каждый день продолжаем сталкиваться с унаследованным кодом, который может быть не покрыт тестами, быть сложным и запутанным. И в результате нам всё же приходится заниматься этим неблагодарным делом.

Сегодня есть множество 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:

  1. Сохраните команду в файл с расширением .star.

  2. Запустите Delve.

  3. Выполните в командной строке команду source flaky.star.

  4. Расставьте точки останова.

  5. Выполните команду 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 можно расширять собственными командами, что делает его очень мощным инструментом.

Дополнительная литература