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

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

Сегодня есть множество IDE, поддерживающих работу с Go и позволяющих отлаживать приложения. На текущий момент для Go представлены два отладчика: GDB (но он не поддерживает многие фичи языка, например Go-рутины) и Delve. Многие IDE используют последний как дефолтный отладчик. И в этой статье я расскажу о возможностях Delve: о том, что умеет сам отладчик, а не что нам предоставляет IDE.

30e87f63742e691c02d5383b0e894083.png

Основы работы с 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)
   }
}

Так выглядит моя директория перед компиляцией:

8c11caef4347bf89ff34f1b762333ad8.png

Теперь скомпилируем программу, выполнив команду go build main.go в командной строке. В результате должно получиться вот что:

1df9a7ddcd1395f4aa729de9b2483dc2.png

Получив бинарный файл, заходим в директорию с ним и выполняем команду dlv debug:

6116befc71f1b6f0173664d3c0fa0958.png

Далее устанавливаем в файле точку останова на строке номер 14, выполнив команду break main.go:14:

fff9731a1770f0f82f457d16802c8e54.png

И запускаем отладку с помощью команды continue:

f097493ad1271fa26f3b33c1220437d2.png

Исполнение программы остановилось на 14-й строке. Теперь можно посмотреть значения переменных:

0ab29ebc6fb3559b45265779aba1c4a5.png

Чтобы продолжить отладку, нужно в командной строке либо выполнить команду next (и тогда выполнится следующая строка кода), либо набрать continue, (и программа выполнится до следующей точки останова).

d5632a8b3e2e17e371bea27f71b47f35.png

Теперь вкратце расскажу про основные команды Delve, с помощью которых вы сможете отлаживать свои приложения:

  • next — следующая строка;

  • step — вход внутрь вызываемой функции:

    8fddacce0961083e695e12221bb9309c.png
  • continue — следующая точка останова (breakpoint):

    d3136f00200d6d1a8dc8c6baef87fbf9.png
  • break — установка точки останова, например break m67 main.go:67;

  • cond — задаёт условия, при которых произойдёт останова на текущей команде отладки. Например, при выполнении команды cond m67 len (array) == 8 сработает останова на этой строке, если в массиве будет восемь элементов;

  • breakpoints — отображает все заданные точки останова;

  • print— распечатывает значение выражения или переменной;

  • vars— выводит значения всех загруженных переменных приложения:

    f0bb0e43916c202e61f61151f8e95bab.png
  • 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 возьмём программу из предыдущего раздела. Пример того, что отобразится в консоли отладчика:  

82dace22be09e1eceed7e09f76b9552e.png

Как видите, программа была перезапущена семь раз, и при каждом выполнении условия срабатывала точка останова. Отлавливать такие вещи вручную в 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)
}

Что происходит на стороне сервера, когда идёт отладка:

23ee7e3c64deb2135963cd7190d0d1fc.png

Тут видно, что было несколько перезапусков приложения. На стороне же приложения будет следующий вывод:

4c882d507eaa4a7c1f015937bca42531.png

Изучим информацию, которую выдаёт Delve:

  • Pid — идентификатор приложения в Linux;

  • Running — запущено ли приложение;

  • Recording — идёт запись информации о процессе;

  • CoreDumping — идёт запись дампа приложения;

  • Threads — информация о потоках исполнения приложения;  

  • breakPoint — информация о сработавшей точке останова.

Подробно про выведенную информацию можно почитать здесь.

Отладка приложения с помощью написания другого приложения позволяет создавать анализаторы поведения программы и автоматизировать проверку своих приложений. Если вам захотелось написать что-то такое, то вам поможет gRPC-клиент.

Заключение

Я только поверхностно ознакомил вас с возможностями Delve. Показал, что мы можем отлаживать код и без IDE. Можно писать анализаторы поведения программ и приложения для отладки своего приложения. Наконец, функциональность Delve можно расширять собственными командами, что делает его очень мощным инструментом.

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

© Habrahabr.ru