Golangのデバッガdelveの使い方

  • 65
    いいね
  • 0
    コメント

はじめに

Goで書いたコードデバッグするのにdelveというデバッガ使ってみたら、なかなかに便利だったので基本的な使い方をまとめておく。

delveのコードは以下のGitHubリポジトリで公開されてるんだけど、
https://github.com/derekparker/delve

使い方の説明が公式ドキュメントにあんまり書いてないので、実際に触ってみて、具体例を挙げて説明していこうかと思う。

インストール

手元の環境はMacなのでbrewで入れた。
※他の環境の場合は以下の公式ドキュメントを参照して下さい。
https://github.com/derekparker/delve/tree/master/Documentation/installation

$ brew install go-delve/delve/delve

インストール中にsudoでキーチェインに証明書を追加しようとするので、管理者権限のパスワードを入力して許可してあげて下さい。

ちなみに本稿執筆時点の手元の環境のバージョンは以下のとおり。

$ go version
go version go1.8.1 darwin/amd64

$ dlv version
Delve Debugger
Version: 0.12.2
Build: v0.12.2

※コマンド名は delve ではなく dlv なことに注意。

使い方

サンプルコード

説明の都合上、適当なサンプルコードを用意して main.go という名前で保存した。

main.go
package main

import "fmt"

func foo() int {
    n := 1
    return n
}

func bar() int {
    n := 2
    return n
}

func main() {
    n := foo()
    n = bar()
    fmt.Printf("n=%d\n", n)
}

デバッガを起動する

まずはdlvコマンドのhelpを見てみる。

$ dlv help
Delve is a source level debugger for Go programs.

Delve enables you to interact with your program by controlling the execution of the process,
evaluating variables, and providing information of thread / goroutine state, CPU register state and more.

The goal of this tool is to provide a simple yet powerful interface for debugging Go programs.

Pass flags to the program you are debugging using `--`, for example:

`dlv exec ./hello -- server --config conf/config.toml`

Usage:
  dlv [command]

Available Commands:
  attach      Attach to running process and begin debugging.
  connect     Connect to a headless debug server.
  debug       Compile and begin debugging main package in current directory, or the package specified.
  exec        Execute a precompiled binary, and begin a debug session.
  run         Deprecated command. Use 'debug' instead.
  test        Compile test binary and begin debugging program.
  trace       Compile and begin tracing program.
  version     Prints version.

Flags:
      --accept-multiclient[=false]: Allows a headless server to accept multiple client connections. Note that the server API is not reentrant and clients will have to coordinate.
      --api-version=1: Selects API version when headless.
      --build-flags="": Build flags, to be passed to the compiler.
      --headless[=false]: Run debug server only, in headless mode.
      --init="": Init file, executed by the terminal client.
  -l, --listen="localhost:0": Debugging server listen address.
      --log[=false]: Enable debugging server logging.
      --wd=".": Working directory for running the program.

Use "dlv [command] --help" for more information about a command.

いくつかサブコマンドがあるけど、 いわゆる普通の go run に相当するのは dlv debug コマンドで

$ dlv help debug
Compiles your program with optimizations disabled, starts and attaches to it.

By default, with no arguments, Delve will compile the 'main' package in the
current directory, and begin to debug it. Alternatively you can specify a
package name and Delve will compile that package instead, and begin a new debug
session.

Usage:
  dlv debug [package] [flags]

Global Flags:
      --accept-multiclient[=false]: Allows a headless server to accept multiple client connections. Note that the server API is not reentrant and clients will have to coordinate.
      --api-version=1: Selects API version when headless.
      --build-flags="": Build flags, to be passed to the compiler.
      --headless[=false]: Run debug server only, in headless mode.
      --init="": Init file, executed by the terminal client.
  -l, --listen="localhost:0": Debugging server listen address.
      --log[=false]: Enable debugging server logging.
      --wd=".": Working directory for running the program.

特に引数の指定がなければ、カレントディレクトリ配下のmainパッケージをデバッグ用のオプションを付けてコンパイルして起動してくれる。

$ dlv debug
Type 'help' for list of commands.
(dlv)

※初回起動時はデバッガはプロセスにアタッチするのにセキュリティ警告が出ますが許可してあげてください。
※dlvコマンドではなくデバッグ対象のコマンド側にコマンドライン引数を与えたい場合は dlv debug -- hoge のように -- で区切った後ろに引数として指定すれば渡せます。

デバッガが起動すると、 (dlv) というプロンプトが出るので、とりあえずhelpを見る。

(dlv) help
The following commands are available:
    args ------------------------ Print function arguments.
    break (alias: b) ------------ Sets a breakpoint.
    breakpoints (alias: bp) ----- Print out info for active breakpoints.
    clear ----------------------- Deletes breakpoint.
    clearall -------------------- Deletes multiple breakpoints.
    condition (alias: cond) ----- Set breakpoint condition.
    continue (alias: c) --------- Run until breakpoint or program termination.
    disassemble (alias: disass) - Disassembler.
    exit (alias: quit | q) ------ Exit the debugger.
    frame ----------------------- Executes command on a different frame.
    funcs ----------------------- Print list of functions.
    goroutine ------------------- Shows or changes current goroutine
    goroutines ------------------ List program goroutines.
    help (alias: h) ------------- Prints the help message.
    list (alias: ls) ------------ Show source code.
    locals ---------------------- Print local variables.
    next (alias: n) ------------- Step over to next source line.
    on -------------------------- Executes a command when a breakpoint is hit.
    print (alias: p) ------------ Evaluate an expression.
    regs ------------------------ Print contents of CPU registers.
    restart (alias: r) ---------- Restart process.
    set ------------------------- Changes the value of a variable.
    source ---------------------- Executes a file containing a list of delve commands
    sources --------------------- Print list of source files.
    stack (alias: bt) ----------- Print stack trace.
    step (alias: s) ------------- Single step through program.
    step-instruction (alias: si)  Single step a single cpu instruction.
    stepout --------------------- Step out of the current function.
    thread (alias: tr) ---------- Switch to the specified thread.
    threads --------------------- Print out info for every traced thread.
    trace (alias: t) ------------ Set tracepoint.
    types ----------------------- Print list of types
    vars ------------------------ Print package variables.
Type help followed by a command for full documentation.

ブレークポイントを設定する

デバッガ内にもいろいろコマンドがあるけど、とりあえずブレークポイントを仕込もう。
break というコマンドがそれ。 help break とすると break の説明が出る。

(dlv) help break
Sets a breakpoint.

        break [name] <linespec>

See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/locspec.md for the syntax of linespec.

See also: "help on", "help cond" and "help clear"

mainパッケージのmain関数にブレークポイントを設定してみる。

(dlv) break main.main
Breakpoint 1 set at 0x10870e8 for main.main() ./main.go:15

ちなみにエイリアスがbなので b main.main でもOK。
慣れるとエイリアスの方がラクなので、以降の説明では break(alias: b) みたいに表記する。

ブレークポイントまで実行

continue(alias: c) でブレークポイントまで実行する。

(dlv) c
> main.main() ./main.go:15 (hits goroutine(1):1 total:1) (PC: 0x10870e8)
    10: func bar() int {
    11:         n := 2
    12:         return n
    13: }
    14:
=>  15: func main() {
    16:         n := foo()
    17:         n = bar()
    18:         fmt.Printf("n=%d\n", n)
    19: }

1行ずつ実行

next (alias: n) で1行ずつ進める。

(dlv) n
> main.main() ./main.go:16 (PC: 0x10870ff)
    11:         n := 2
    12:         return n
    13: }
    14:
    15: func main() {
=>  16:         n := foo()
    17:         n = bar()
    18:         fmt.Printf("n=%d\n", n)
    19: }
(dlv) n
> main.main() ./main.go:17 (PC: 0x108710d)
    12:         return n
    13: }
    14:
    15: func main() {
    16:         n := foo()
=>  17:         n = bar()
    18:         fmt.Printf("n=%d\n", n)
    19: }

変数の値の表示

print (alias: p) で変数nの値が表示できる。

(dlv) p n
1

ローカル変数の一覧表示

locals でローカル変数の一覧が見れる。

(dlv) locals
n = 1

変数の値の上書き

set で変数の値の上書きができる

(dlv) set n = 0
(dlv) p n
0

ソースコードの表示

list (alias: ls) で現在位置のソースコードの表示ができる。

(dlv) ls
> main.main() ./main.go:17 (PC: 0x108710d)
    12:         return n
    13: }
    14:
    15: func main() {
    16:         n := foo()
=>  17:         n = bar()
    18:         fmt.Printf("n=%d\n", n)
    19: }

関数の中にステップイン

step (alias: s) で関数の中にステップイン。

(dlv) s
> main.bar() ./main.go:10 (PC: 0x1087090)
     5: func foo() int {
     6:         n := 1
     7:         return n
     8: }
     9:
=>  10: func bar() int {
    11:         n := 2
    12:         return n
    13: }
    14:
    15: func main() {

この例だとユーザ定義の関数だけだけど、importしたライブラリ関数の中なども潜っていけます。

スタックトレースの表示

stack (alias: bt) でスタック(バックトレース)の表示

(dlv) bt
0  0x0000000001087090 in main.bar
   at ./main.go:10
1  0x0000000001087112 in main.main
   at ./main.go:17
2  0x000000000102752a in runtime.main
   at /usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185
3  0x000000000104c3a1 in runtime.goexit
   at /usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

スタックトレース上の指定のフレームでデバッグコマンドを実行

frame でスタックトレース上の指定のフレーム上でデバッグコマンドを実行できる

(dlv) frame 1 ls
    12:         return n
    13: }
    14:
    15: func main() {
    16:         n := foo()
=>  17:         n = bar()
    18:         fmt.Printf("n=%d\n", n)
    19: }

なので変数の状態を見たりもできる。

(dlv) frame 1 locals
n = 0

ちなみにさらにスタックトレースを遡るとgoのランタイムのコードが見える。

(dlv) frame 2 ls
   180:                 // A program compiled with -buildmode=c-archive or c-shared
   181:                 // has a main, but it is not executed.
   182:                 return
   183:         }
   184:         fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
=> 185:         fn()
   186:         if raceenabled {
   187:                 racefini()
   188:         }
   189:
   190:         // Make racy client program work: if panicking on

ステップアウトで関数を抜ける

stepout で関数を抜ける

(dlv) ls
> main.bar() ./main.go:10 (PC: 0x1087090)
     5: func foo() int {
     6:         n := 1
     7:         return n
     8: }
     9:
=>  10: func bar() int {
    11:         n := 2
    12:         return n
    13: }
    14:
    15: func main() {
(dlv) stepout
> main.main() ./main.go:17 (PC: 0x1087112)
    12:         return n
    13: }
    14:
    15: func main() {
    16:         n := foo()
=>  17:         n = bar()
    18:         fmt.Printf("n=%d\n", n)
    19: }
(dlv) p n
0
(dlv) n
> main.main() ./main.go:18 (PC: 0x108711b)
    13: }
    14:
    15: func main() {
    16:         n := foo()
    17:         n = bar()
=>  18:         fmt.Printf("n=%d\n", n)
    19: }
(dlv) p n
2

デバッガを終了する

exit (alias: quit | q) でデバッガを終了する。

(dlv) q

お疲れ様でした。

まとめ

Goで書いたコードデバッグするのにdelveというデバッガの基本的な使い方を説明しました。

他にもgoroutineの切り替えしたり、起動してるプロセスにattachしたり、いろいろ機能がありそうです。
使いこなせるようになるとデバッグが捗りそうですね。