Goで開発してる時のデバッグ

blog.golang.org

4月20日に出たこの Go Developer Survey 2019 Results 眺めていて、 I depend upon the following techniques when developing in Go が気になった。
これは Go で開発する時にどういう手法に頼ってるかみたいな内容で、一番は fmt.Print() or similar で text log でした。

たしかに自分もよく text log だしてたりするなと思った。
後は Go は動かす前にコンパイルするから、最低限動くもののフィードバックはここで受け取って修正できているからあまり困っていないのかもしれない。

survey では text log の次は「 Delv や GDB のような debugger を local machine で使う」でした。
自分はどっちも使ったことなかった。
ということで試したので、そのメモをこの記事に書く。

GDB

GDBGNU DeBugger の略らしい。
Go のためのやつかと思ったけど C, C++ だったり gccコンパイルされたプログラムのデバッグとかに使われるらしい。
sourceware.org

golang.org

僕の普段の開発機は Mac なので今回も gdbMac で動かしていこうと思ったのですが、署名とかが必要になるっぽくて面倒だったので docker run -it --rm golang:1.14.3 bash でコンテナ立ててそこで検証することにしました。

sourceware.org

golang:1.14.3 の image を使った場合は apt-get update, apt-get install gdb すればとりあえずインストールはされる。

今回は下記のプログラムを使います。

package main

import (
    "fmt"
)

type user struct {
    Name string
    Age  int
}

func main() {
    fmt.Println("start")

    u := &user{
        Name: "hatappi",
        Age:  28,
    }

    echo()

    fmt.Printf("name is %s, age is %d\n", u.Name, u.Age)

    fmt.Println("end")
}

func echo() {
    fmt.Println("echo~~")
}

Go のドキュメントに書かれているように、Go は build 時に最適化が走るのでそれを無効にしつつビルドします。
さらに Go 1.11 以降はデバッグ情報が圧縮されるのでそれも無効にします。

$ go build \
   -ldflags=-w \
   -gcflags=all="-N -l" \
   -ldflags=-compressdwarf=false \
   -o main \
   main.go

次に gdb main として下記のようなものが出たらいったん ok です。
Go の extension が load されていない場合は source /usr/local/go/src/runtime/runtime-gdb.py で go の extension を load します。

実行後は l で source code を表示できて l main.echo のようにすると指定した関数のソースコードを表示できます。

(gdb) l main.echo
13              echo()
14
15              fmt.Println("end")
16      }
17
18      func echo() {
19              fmt.Println("echo~~")
20      }
(gdb) 

l 10 を実行すると10行目を中心に前後の行を表示してくれます。
run を実行するとプログラムを実行します。
しかしこのままだと処理が終わってしまいます。
そこで break を使ってブレークポイントを設定します。

(gdb) b 20
Breakpoint 1 at 0x4afb8d: file /root/main.go, line 20.

設定したブレークポイントinfo b で確認できます。
run を実行すればブレークポイントを設定したところで処理がとまってくれるます。
止まったところで様々なコマンドを実行することができます。
例えば定義されている変数を見る info localsp u.Name とかで特定の変数の値を見ることができます。

(gdb) p u.Name
$1 = 0x4de43d "hatappi"

それ以外だと info goroutines で Go routine の状況もみられます。
さらに変数も変更することができます。

(gdb) p u.Age 
$2 = 28
(gdb) set variable u.Age=100
(gdb) p u.Age
$3 = 100

処理を続行したい時は c を入力すれば処理が継続されます。

Delve

github.com

GDB は Go 以外にも使用できていたけどこの Delve は Delve is a debugger for the Go programming language. とあるように Go のためのデバッグツールです。

delve は xcode-select --install して go get github.com/go-delve/delve/cmd/dlv すれば使えます。
https://github.com/go-delve/delve/blob/master/Documentation/installation/osx/install.md

go get してくるだけなのでさくっと使えて良さそう。

今回も GDB の時と同じく下記のプログラムを使います。

package main

import (
    "fmt"
)

type user struct {
    Name string
    Age  int
}

func main() {
    fmt.Println("start")

    u := &user{
        Name: "hatappi",
        Age:  28,
    }

    echo()

    fmt.Printf("name is %s, age is %d\n", u.Name, u.Age)

    fmt.Println("end")
}

func echo() {
    fmt.Println("echo~~")
}

起動は main パッケージと同じディレクトリにいれば dlv debug で起動できます。

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

どういったことができるかは起動後の help でもみれますし下記からドキュメントを参照することもできます。
https://github.com/go-delve/delve/tree/master/Documentation/cli

ソースコードを表示する時は GDB と同じく l 40 とか l main.echo で確認することができます。

ブレークポイントbreak main.main で設定して c で実行すると main がよばれたところでとまります。
さらに main 関数内でとめたいところを break 20 で指定してもう一度 c で処理を継続します。

ローカル変数をみたい時は locals で 特定の変数を使いたい時は print を使います。

(dlv) print u
*main.user {Name: "hatappi", Age: 28}

変数は set u.Age = 100 のように set を使うことで変更できます。

delve は他にも起動中のプロセスにattach できる dlv attach があったりして便利そう。