具体例で理解するGo言語のデバッグツールDelveの利用方法

Go言語

Go言語のデバッガはDelveGDBが人気です。
GDBのドキュメントには以下のような記載があり、Delveの利用を推奨しています。

Note that Delve is a better alternative to GDB when debugging Go programs built with the standard toolchain.
(Delve は、標準ツールチェーンでビルドされた Go プログラムをデバッグする際に、GDB よりも優れた代替手段であることに注意してください。)

今回はDelveを利用したGo言語のデバッグ方法について紹介します。

Delveの主なコマンド一覧

デバッグをする際によく利用するDelveのコマンドをピックアップしました。

内容 コマンド ショートコード
実行 dlv debug [ファイル名] N/A
再実行 restart r
終了 exit
quit
q
ブレークポイントの作成 break [ファイル名]:[行数] b [ファイル名]:[行数]
ブレークポイントの確認 breakpoints bp
次のブレークポイントへ移動 continue c
次の行へ移動 next n
ステップイン step s
ステップアウト stepout N/A
コードの表示 ls [関数名]ls [パッケージ].[関数名] N/A
ローカル変数の表示 locals N/A
変数の表示 print [変数] p [変数]
変数の値の変更 set [変数] = [値] N/A
スタックトレースの表示 stack bt
スタックトレース上でデバッグを実行 frame [番号] [コマンド] N/A
ヘルプの表示 help h

Delveにはそのほかにも多数のコマンドが用意されています。
詳細はdelve/Documentation/cli/README.mdをご参考ください。

Delveのインストール方法

Delveのインストール手順はdelve/Documentation/installation/README.mdに記載されています。

$ go install github.com/go-delve/delve/cmd/dlv@latest

### dlvコマンドが存在していればOK
$ which dlv

/go/bin/dlv

Delveを利用した具体的なデバッグ方法

以下のコードを例に、Delveを利用した具体的なデバッグ方法について紹介します。

main.go

package main

import "fmt"

func greeting(name string) string {
    beginning := "Hello! "
    message := beginning + name

    return message
}

func main() {
  name := "Bob"
    message := greeting(name)

    fmt.Println(message)
}

Delveの起動と終了

dlv debug [ファイル名]でデバッグモードになります。Delveを終了する場合はqを入力します。

$ dlv debug main.go
(dlv)

(dlv)q

ブレークポイントを追加し、移動する

bでブレークポイントを作成し、cでブレークポイントへ移動できます。
rでブレークポイントを残したままデバッグの再実行ができます。

$ dlv debug main.go
(dlv) b main.go:14
Breakpoint 1 set at 0x4b6964 for main.main() ./main.go:14

(dlv) c
     9:     return message
    10: }
    11:
    12: func main() {
    13:   name := "Bob"
=>  14:     message := greeting(name)
    15:
    16:     fmt.Println(message)
    17: }

(dlv) r
Process restarted with PID 40673

(dlv) c
> main.main() ./main.go:14 (hits goroutine(1):1 total:1) (PC: 0x10cb964)
     9:     return message
    10: }
    11:
    12: func main() {
    13:   name := "Bob"
=>  14:     message := greeting(name)
    15:
    16:     fmt.Println(message)
    17: }

デバッグ中にソースコードを確認する

lsでデバッグ中にソースコードの確認ができます。

$ dlv debug main.go
(dlv) b main.go:14
(dlv) c
     9:     return message
    10: }
    11:
    12: func main() {
    13:   name := "Bob"
=>  14:     message := greeting(name)
    15:
    16:     fmt.Println(message)
    17: }

(dlv) ls main.greeting # greetingメソッドの表示
Showing /Users/301184/work/delve/main.go:5 (PC: 0x10cb873)
     1: package main
     2:
     3: import "fmt"
     4:
     5: func greeting(name string) string {
     6:     beginning := "Hello! "
     7:     message := beginning + name
     8:
     9:     return message
    10: }

(dlv) ls greeting # greetingメソッドの表示
Showing /Users/301184/work/delve/main.go:5 (PC: 0x10cb873)
     1: package main
     2:
     3: import "fmt"
     4:
     5: func greeting(name string) string {
     6:     beginning := "Hello! "
     7:     message := beginning + name
     8:
     9:     return message
    10: }

(dlv) ls # 現時点(ブレークポイント)のコードを表示
> main.main() ./main.go:16 (PC: 0x10cb98a)
    11:
    12: func main() {
    13:   name := "Bob"
    14:     message := greeting(name)
    15:
=>  16:     fmt.Println(message)
    17: }

ブレークポイント時点での変数の中身を確認する

localsで値の確認ができます。
たとえば、14行目ではnameの値が取得できます。

$ dlv debug main.go
(dlv) b main.go:14
(dlv) c
     9:     return message
    10: }
    11:
    12: func main() {
    13:   name := "Bob"
=>  14:     message := greeting(name)
    15:
    16:     fmt.Println(message)
    17: }

(dlv) locals
name = "Bob"

nで次の行へ移動すると、nameと、14行目でセットされたmessageが表示されます。

(dlv) n
> main.main() ./main.go:16 (PC: 0x10cb98a)
    11:
    12: func main() {
    13:   name := "Bob"
    14:     message := greeting(name)
    15:
=>  16:     fmt.Println(message)
    17: }

(dlv) locals
name = "Bob"
message = "Hello! Bob"

(div) p name
"Bob"

ステップインとステップアウト

sでステップインできます。
たとえば14行目でsを実行すると、14行目で実行されているgreetingメソッドのデバッグができます。

$ dlv debug main.go
(dlv) b main.go:14
(dlv) c
     9:     return message
    10: }
    11:
    12: func main() {
    13:   name := "Bob"
=>  14:     message := greeting(name)
    15:
    16:     fmt.Println(message)
    17: }

(dlv) s
> main.greeting() ./main.go:5 (PC: 0x10cb873)
     1: package main
     2:
     3: import "fmt"
     4:
=>   5: func greeting(name string) string {
     6:     beginning := "Hello! "
     7:     message := beginning + name
     8:
     9:     return message
    10: }

stepoutでステップアウトします。

(dlv) ls
> main.greeting() ./main.go:5 (PC: 0x10cb873)
     1: package main
     2:
     3: import "fmt"
     4:
=>   5: func greeting(name string) string {
     6:     beginning := "Hello! "
     7:     message := beginning + name
     8:
     9:     return message
    10: }

(dlv) stepout
> main.main() ./main.go:14 (PC: 0x10cb976)
Values returned:
    ~r1: "Hello! Bob"

     9:     return message
    10: }
    11:
    12: func main() {
    13:   name := "Bob"
=>  14:     message := greeting(name)
    15:
    16:     fmt.Println(message)
    17: }

ブレークポイントから次のブレークポイントへ移動する

cでブレークポイントを移動できます。

(dlv) b main.go:14
(dlv) b main.go:16

(dlv) c
> main.main() ./main.go:14 (hits goroutine(1):1 total:1) (PC: 0x10cb964)
     9:     return message
    10: }
    11:
    12: func main() {
    13:   name := "Bob"
=>  14:     message := greeting(name)
    15:
    16:     fmt.Println(message)
    17: }

(dlv) c
> main.main() ./main.go:16 (hits goroutine(1):1 total:1) (PC: 0x10cb98a)
    11:
    12: func main() {
    13:   name := "Bob"
    14:     message := greeting(name)
    15:
=>  16:     fmt.Println(message)
    17: }

スタックトレースを利用してデバッグする

btでスタックトレースを表示し、frameで実行したいコマンドと位置を決定します。

$ dlv debug main.go
(dlv) b main.go:9
(dlv) c
> main.greeting() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x10cb8cd)
     4:
     5: func greeting(name string) string {
     6:     beginning := "Hello! "
     7:     message := beginning + name
     8:
=>   9:     return message
    10: }
    11:
    12: func main() {
    13:   name := "Bob"
    14:     message := greeting(name)

(dlv) bt
0  0x00000000010cb8cd in main.greeting
   at ./main.go:9
1  0x00000000010cb976 in main.main
   at ./main.go:14
2  0x000000000103b6e3 in runtime.main
   at /usr/local/Cellar/go/1.16.3/libexec/src/runtime/proc.go:225
3  0x000000000106eb01 in runtime.goexit
   at /usr/local/Cellar/go/1.16.3/libexec/src/runtime/asm_amd64.s:1371

(dlv) frame 1 locals # ./main.go:14 で localsを実行
name = "Bob"

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考