Carpe Diem

備忘録。https://github.com/jun06t

delveでGolangのデバッグ

概要

Golangデバッグをする際にdelveというツールがオススメです。

環境

  • golang v1.8.3
  • delve 1.0.0-rc.1

インストー

brewでもインストールできますが、単体テストの実行の際に上手く動かないことがあったのでgo getの方をオススメします。

$ xcode-select --install
$ go get -u github.com/derekparker/delve/cmd/dlv

通常の使い方

以下のコードに対して実行するとします。

func main() {
    a := Hoge("main")

    fmt.Println(a)
}

func Hoge(m string) string {
    hoge := "hoge"
    fuga := m + hoge + "fuga"

    return fuga
}

起動

$ dlv debug hoge.go
(dlv) 

ソースの確認

ls <func>ls <pkg>.<func>の形式で表示できます。

関数指定の場合

(dlv) ls Hoge
  10:      fmt.Println(a)
  11:   
  12:      fmt.Println("end")
  13:  }
  14:   
  15:  func Hoge(m string) string {
  16:      hoge := "hoge"
  17:      fuga := m + hoge + "fuga"
  18:   
  19:      return fuga
  20:  }

パッケージ指定の場合

(dlv) ls main.main
   1:  package main
   2:   
   3:  import "fmt"
   4:   
   5:  func main() {
   6:      fmt.Println("start")
   7:   
   8:      a := Hoge("main")
   9:   
  10:      fmt.Println(a)

ブレークポイント設定

b <func>b <filename>:<line>で設定できます。オススメは後者です。

(dlv) b hoge.go:6
Breakpoint 1 set at 0x1087223 for main.Hoge() ./hoge.go:11

開始

ブレークポイントをセットしたらデバッグを開始します。

(dlv) c
> main.main() ./hoge.go:6 (hits goroutine(1):1 total:1) (PC: 0x1087122)
     1:    package main
     2: 
     3:    import "fmt"
     4: 
     5:    func main() {
=>   6:     fmt.Println("start")
     7: 
     8:        a := Hoge("main")
     9: 
    10:        fmt.Println(a)
    11: 

ちゃんとブレークポイントで止まります。

1行ずつ実行

nを入力します。

(dlv) n
start
> main.main() ./hoge.go:8 (PC: 0x10871e5)
     3:    import "fmt"
     4: 
     5:    func main() {
     6:        fmt.Println("start")
     7: 
=>   8:     a := Hoge("main")
     9: 
    10:        fmt.Println(a)
    11: 
    12:        fmt.Println("end")
    13:    }

1つずつ進みます。

ステップイン

関数の中に入りたい時はsを入力

(dlv) s
> main.Hoge() ./hoge.go:15 (PC: 0x10873c3)
    10:        fmt.Println(a)
    11: 
    12:        fmt.Println("end")
    13:    }
    14: 
=>  15: func Hoge(m string) string {
    16:        hoge := "hoge"
    17:        fuga := m + hoge + "fuga"
    18: 
    19:        return fuga
    20:    }

すると定義元まで行ってくれます。

変数の確認

localsで表示できます。これができればもうプリントデバッグはしなくて済みますね。

(dlv) locals
hoge = (unreadable could not read string at 0x1 due to protocol error E08 during memory read for packet $m1,6)
fuga = ""

今の行数だとまだ代入されてないですが、処理を進めてから実行してみます。

(dlv) n
> main.Hoge() ./hoge.go:17 (PC: 0x10873fe)
    12:        fmt.Println("end")
    13:    }
    14: 
    15:    func Hoge(m string) string {
    16:        hoge := "hoge"
=>  17:     fuga := m + hoge + "fuga"
    18: 
    19:        return fuga
    20:    }
(dlv) locals
hoge = "hoge"
fuga = ""

hoge :=の処理が終わった後は中身が代入されてますし、

(dlv) n
> main.Hoge() ./hoge.go:19 (PC: 0x108745f)
    14: 
    15:    func Hoge(m string) string {
    16:        hoge := "hoge"
    17:        fuga := m + hoge + "fuga"
    18: 
=>  19:     return fuga
    20:    }
(dlv) locals
hoge = "hoge"
fuga = "mainhogefuga"

fugaの方も代入後はちゃんと変数に値が入っています。

再実行

ブレークポイントを残したままもう一度処理を再実行したい場合はrestartです。

(dlv) restart
Process restarted with PID 55704
(dlv) c
> main.main() ./hoge.go:6 (hits goroutine(1):1 total:1) (PC: 0x1087122)
     1:    package main
     2: 
     3:    import "fmt"
     4: 
     5:    func main() {
=>   6:     fmt.Println("start")
     7: 
     8:        a := Hoge("main")
     9: 
    10:        fmt.Println(a)
    11: 

単体テストでの使い方

小さなプログラムであれば、問題の箇所にブレークポイントをセットしてそこまで実行させればいいですが、大きなプログラムだとそれは大変です。
なので単体テストでチェックします。

対象のテスト

func TestHoge(t *testing.T) {
    tests := []struct {
        in  string
        out string
    }{
        {"1", "1hogefuga"},
        {"tmp", "tmphogefuga"},
    }

    for _, v := range tests {
        out := Hoge(v.in)
        if out != v.out {
            t.Errorf("input %s\n, get %s\n, want %s\n", v.in, out, v.out)
        }
    }
}

実行方法

dlv testを使います。また、通常単体テストを指定する時にgo test -run TestHogeと実行するように、-test.runオプションを後ろにつけます。

$ dlv test -- -test.run TestHoge
(dlv) 

関数にブレークポイントをセットします。

(dlv) b hoge.go:15
Breakpoint 1 set at 0x10efd13 for _/Users/a13156.Hoge() ./hoge.go:15

実行してみます。

(dlv) c
> _/Users/jun06t.Hoge() ./hoge.go:15 (hits goroutine(5):1 total:1) (PC: 0x10efd13)
    10:        fmt.Println(a)
    11: 
    12:        fmt.Println("end")
    13:    }
    14: 
=>  15: func Hoge(m string) string {
    16:        hoge := "hoge"
    17:        fuga := m + hoge + "fuga"
    18: 
    19:        return fuga
    20:    }

変数を確認してみると、ちゃんとテストの値が入っていることが分かります。

(dlv) n
> _/Users/jun06t.Hoge() ./hoge.go:19 (PC: 0x10efdaf)
    14: 
    15:    func Hoge(m string) string {
    16:        hoge := "hoge"
    17:        fuga := m + hoge + "fuga"
    18: 
=>  19:     return fuga
    20:    }
(dlv) locals
hoge = "hoge"
fuga = "1hogefuga"

その他ハマった点

lldb-serverが実行できない

could not launch process: exec: "lldb-server": executable file not found in $PATH

というエラーです。以下を実行したら僕は直りました。

$ xcode-select --install

getsockopt: connection refused

could not launch process: dial tcp :49754: getsockopt: connection refused

というエラーです。githubのissueを見たところ、僕の環境では結局brewをやめてgo getで入れたら直りました。

バイナリに対して実行したら変数が表示されない

delveはdlv exec <binary>と実行すればバイナリに対してもデバッグできます。
しかしgoのビルドは通常だと最適化され、実行された関数や変数の値はレジスタへ保存されるため、そのままだとdelveで変数などが上手く表示できません。

(dlv) locals
(no locals)

なのでビルド時には最適化を無効化するために以下のオプションを付けます。

$ go build -gcflags '-N -l' sample.go

こうして作ったバイナリであれば変数もきちんと表示されました。

ソース