CLOVER🍀

That was when it all began.

Goアプリケーションをデバッグしたい

これは、なにをしたくて書いたもの?

Goで書かれたアプリケーションをデバッグする方法を、押さえておきたいな、と思いまして。

Goアプリケーションのデバッグ

Goのドキュメントでデバッグについて書かれているのは、こちらのページです。

Debugging Go Code with GDB - The Go Programming Language

gdbデバッグできるようですが、標準のgc Goコンパイラーを使っている場合はDelveというものを使った方が良いとも
書かれています。

GitHub - go-delve/delve: Delve is a debugger for the Go programming language.

デフォルトのコンパイラーはgcのようです。

GDB does not understand Go programs well. The stack management, threading, and runtime contain aspects that differ enough from the execution model GDB expects that they can confuse the debugger and cause incorrect results even when the program is compiled with gccgo. As a consequence, although GDB can be useful in some situations (e.g., debugging Cgo code, or debugging the runtime itself), it is not a reliable debugger for Go programs, particularly heavily concurrent ones. Moreover, it is not a priority for the Go project to address these issues, which are difficult.

環境

今回の環境は、こちらです。

$ go version
go version go1.15.7 linux/amd64

また、デバッグ用のコードも用意しておきましょう。

$ go mod init debug-go-app
go: creating new go.mod: module debug-go-app

go.mod

module debug-go-app

go 1.15

単純なコードを用意。

main.go

package main

import (
    "debug-go-app/sub"
    "fmt"
)

func main() {
    message := sub.GetMessage("World")

    fmt.Println(message)

    languages := []string{"Java", "Go", "Python", "Perl"}

    for _, language := range languages {
        m := sub.GetMessage(language)

        fmt.Println(m)
    }
}

sub/message.go

package sub

import "fmt"

func GetMessage(word string) string {
    return fmt.Sprintf("Hello, %s!!", word)
}

gdbでGoアプリケーションをデバッグする

まずは、gdbデバッグしてみましょう。

どうやら、ビルド時にオプションを付けた方がよさそうです。最適化が効いていると、うまくデバッグできない可能性があるみたいです。

The code generated by the gc compiler includes inlining of function invocations and registerization of variables. These optimizations can sometimes make debugging with gdb harder. If you find that you need to disable these optimizations, build your program using go build -gcflags=all="-N -l".

Debugging Go Code with GDB - The Go Programming Language

というわけで、指示通りgcflags=all='-N -l'を付けてビルド。

$ go build -gcflags=all='-N -l'

アプリケーションを、gdbで起動してみます。

$ gdb ./debug-go-app 
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./debug-go-app...
warning: File "/usr/share/go-1.15/src/runtime/runtime-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
    add-auto-load-safe-path /usr/share/go-1.15/src/runtime/runtime-gdb.py
line to your configuration file "$HOME/.gdbinit".
To completely disable this security protection add
    set auto-load safe-path /
line to your configuration file "$HOME/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual.  E.g., run from the shell:
    info "(gdb)Auto-loading safe path"
(gdb) 

いろいろ言われていますが、警告も混じっていますね。以下の内容で、$HOME/.gdbinitというファイルを作った方が良さそうです。

$HOME/.gdbinit

add-auto-load-safe-path /usr/share/go-1.15/src/runtime/runtime-gdb.py

警告が出なくなりました。

$ gdb ./debug-go-app 
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./debug-go-app...
Loading Go Runtime support.

listで関数の内容を表示してみます。

(gdb) list main.main
3   import (
4       "debug-go-app/sub"
5       "fmt"
6   )
7   
8   func main() {
9       message := sub.GetMessage("World")
10  
11      fmt.Println(message)
12

ファイル名と行番号指定で表示。

(gdb) list main.go:5
1   package main
2   
3   import (
4       "debug-go-app/sub"
5       "fmt"
6   )
7   
8   func main() {
9       message := sub.GetMessage("World")
10

サブディレクトリ内のファイル。

(gdb) list sub/message.go:1
1   package sub
2   
3   import "fmt"
4   
5   func GetMessage(word string) string {
6       return fmt.Sprintf("Hello, %s!!", word)
7   }

サブディレクトリ…というかパッケージ内の関数の内容を表示するには…?

(gdb) list sub.GetMessage
Function "sub.GetMessage" not defined.

info functionsで、認識している関数をリストアップしてみます。

(gdb) info functions
All defined functions:

File /path/to/main.go:
    void main.main(void);

File /path/to/sub/message.go:
    void debug-go-app/sub.GetMessage(string, string);

File /usr/lib/go-1.15/src/errors/errors.go:
    void errors.(*errorString).Error;
    void errors.New(string, error);

File /usr/lib/go-1.15/src/errors/wrap.go:
    void errors.init(void);

〜省略〜

よく見ると、自分が作った関数が混じっています。

こうですね。

(gdb) list debug-go-app/sub.GetMessage
1   package sub
2   
3   import "fmt"
4   
5   func GetMessage(word string) string {
6       return fmt.Sprintf("Hello, %s!!", word)
7   }

runでプログラムの実行。

(gdb) run
Starting program: /path/to/debug-go-app 
[New LWP 26218]
[New LWP 26219]
[New LWP 26220]
[New LWP 26221]
Hello, World!!
Hello, Java!!
Hello, Go!!
Hello, Python!!
Hello, Perl!!
[LWP 26221 exited]
[LWP 26220 exited]
[LWP 26219 exited]
[LWP 26218 exited]
[Inferior 1 (process 26217) exited normally]

breakで、指定の関数にブレークポイントを付けられます。

(gdb) break debug-go-app/sub.GetMessage
Breakpoint 1 at 0x4ba6a0: file /path/to/sub/message.go, line 5.

実行すると、ブレークポイントの場所で止まります。

(gdb) run
Starting program: /path/to/debug-go-app 
[New LWP 27081]
[New LWP 27082]
[New LWP 27083]
[New LWP 27084]
[New LWP 27085]

Thread 1 "debug-go-app" hit Breakpoint 1, debug-go-app/sub.GetMessage (word="World", Python Exception <class 'OverflowError'> signed integer is greater than maximum: 
~r1=) at /path/to/sub/message.go:5
5   func GetMessage(word string) string {

printまたはpで、変数の内容を表示。

(gdb) p word
$1 = "World"


(gdb) print word
$2 = "World"

btスタックトレースの表示。

(gdb) bt
#0  debug-go-app/sub.GetMessage (word="World", Python Exception <class 'OverflowError'> signed integer is greater than maximum: 
~r1=) at /path/to/sub/message.go:5
#1  0x00000000004ba82b in main.main () at /path/to/main.go:9

continueで再開します。

(gdb) continue
Continuing.
Hello, World!!

Thread 1 "debug-go-app" hit Breakpoint 1, debug-go-app/sub.GetMessage (word="Java", ~r1=<error reading variable: Cannot access memory at address 0x1>)
    at /path/to/sub/message.go:5
5   func GetMessage(word string) string {

nextで1行ずつ進めていきます。

(gdb) next
6       return fmt.Sprintf("Hello, %s!!", word)
(gdb) next
main.main () at /path/to/main.go:18
18          fmt.Println(m)
(gdb) next
Hello, Java!!
15      for _, language := range languages {
(gdb) next
16          m := sub.GetMessage(language)
(gdb) next

Thread 1 "debug-go-app" hit Breakpoint 1, debug-go-app/sub.GetMessage (word="Go", ~r1=<error reading variable: Cannot access memory at address 0x1>)
    at /path/to/sub/message.go:5
5   func GetMessage(word string) string {
(gdb) next
6       return fmt.Sprintf("Hello, %s!!", word)
(gdb) next
main.main () at /path/to/main.go:18
18          fmt.Println(m)

現在のブレークポイントの一覧は、info breakpointsで表示できます。

(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004ba6a0 in debug-go-app/sub.GetMessage at /paht/to/sub/message.go:5
    breakpoint already hit 3 times

deleteで、指定の番号のブレークポイントを削除できます。

(gdb) delete 1

ブレークポイントがなくなりました。

(gdb) info breakpoints
No breakpoints or watchpoints.

再開して、終了。

(gdb) continue
Continuing.
Hello, Go!!
Hello, Python!!
Hello, Perl!!
Couldn't get registers: そのようなプロセスはありません.
Couldn't get registers: そのようなプロセスはありません.
(gdb) [LWP 27085 exited]
[LWP 27084 exited]
[LWP 27083 exited]
[LWP 27082 exited]
[LWP 27081 exited]
[Inferior 1 (process 27077) exited normally]

ビルドした結果は、1度削除しておきます。

$ rm debug-go-app

DelveでGoアプリケーションをデバッグする

次は、DelveでGoアプリケーションをデバッグしてみましょう。

GitHub - go-delve/delve: Delve is a debugger for the Go programming language.

インストール方法は、こちらを参照。

delve/install.md at master · go-delve/delve · GitHub

Goプロジェクト外のディレクトリで、以下のコマンドを実行してインストール。

$ GO111MODULE=on go get github.com/go-delve/delve/cmd/dlv

dlvというコマンドがインストールされました。今回扱うDelveは、1.6.0です。

$ dlv version
Delve Debugger
Version: 1.6.0
Build: $Id: 8cc9751909843dd55a46e8ea2a561544f70db34d $

というわけで、以降はこちらのドキュメントを見ながらDelveを使っていきましょう。

delve/Documentation at v1.6.0 · go-delve/delve · GitHub

使い方は、こちらを見るとよさそうです。

delve/dlv.md at v1.6.0 · go-delve/delve · GitHub

Delveは、サブコマンドを使って実行するようです。たとえば、以下のようなサブコマンドがあります(全部は載せていません)。

まずはdebugが基本かな?と思うので、debugサブコマンドでmain.goを指定して実行。

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

helpと入力するとコマンドが見れるというので、ヘルプを見てみましょう。

(dlv) help
The following commands are available:

Running the program:
    call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!)
    continue (alias: c) --------- Run until breakpoint or program termination.
    next (alias: n) ------------- Step over to next source line.
    rebuild --------------------- Rebuild the target executable and restarts it. It does not work if the executable was not built by delve.
    restart (alias: r) ---------- Restart process.
    step (alias: s) ------------- Single step through program.
    step-instruction (alias: si)  Single step a single cpu instruction.
    stepout (alias: so) --------- Step out of the current function.

Manipulating breakpoints:
    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.
    on --------------------- Executes a command when a breakpoint is hit.
    trace (alias: t) ------- Set tracepoint.

Viewing program variables and memory:
    args ----------------- Print function arguments.
    display -------------- Print value of an expression every time the program stops.
    examinemem (alias: x)  Examine memory:
    locals --------------- Print local variables.
    print (alias: p) ----- Evaluate an expression.
    regs ----------------- Print contents of CPU registers.
    set ------------------ Changes the value of a variable.
    vars ----------------- Print package variables.
    whatis --------------- Prints type of an expression.

Listing and switching between threads and goroutines:
    goroutine (alias: gr) -- Shows or changes current goroutine
    goroutines (alias: grs)  List program goroutines.
    thread (alias: tr) ----- Switch to the specified thread.
    threads ---------------- Print out info for every traced thread.

Viewing the call stack and selecting frames:
    deferred --------- Executes command in the context of a deferred call.
    down ------------- Move the current frame down.
    frame ------------ Set the current frame, or execute command on a different frame.
    stack (alias: bt)  Print stack trace.
    up --------------- Move the current frame up.

Other commands:
    config --------------------- Changes configuration parameters.
    disassemble (alias: disass)  Disassembler.
    edit (alias: ed) ----------- Open where you are in $DELVE_EDITOR or $EDITOR
    exit (alias: quit | q) ----- Exit the debugger.
    funcs ---------------------- Print list of functions.
    help (alias: h) ------------ Prints the help message.
    libraries ------------------ List loaded dynamic libraries
    list (alias: ls | l) ------- Show source code.
    source --------------------- Executes a file containing a list of delve commands
    sources -------------------- Print list of source files.
    types ---------------------- Print list of types

Type help followed by a command for full documentation.

この内容は、こちらのページでも見ることができます。

delve/Documentation/cli at v1.6.0 · go-delve/delve · GitHub

関数の一覧を表示するには、funcsで。

(dlv) funcs

Go自体の関数も大量に現れますが、自分が作成した関数もその中に含まれています。

debug-go-app/sub.GetMessage

...

main.main

正規表現で絞り込むことも可能です。

(dlv) funcs ^main.*
main.main


(dlv) funcs ^debug-go.+
debug-go-app/sub.GetMessage

ソースファイルの一覧の場合は、`sourcesで。

(dlv) sources
/path/to/main.go
/path/to/sub/message.go
/usr/lib/go-1.15/src/errors/errors.go
/usr/lib/go-1.15/src/errors/wrap.go
/usr/lib/go-1.15/src/fmt/doc.go
/usr/lib/go-1.15/src/fmt/errors.go
/usr/lib/go-1.15/src/fmt/format.go
/usr/lib/go-1.15/src/fmt/print.go
/usr/lib/go-1.15/src/fmt/scan.go

〜省略〜


(dlv) sources main.go|message.go
/path/to/main.go
/path/to/sub/message.go

では、ブレークポイントをつけましょう。ヘルプを見てみます。

(dlv) help break
Sets a breakpoint.

    break [name] <linespec>

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

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

breakの指定の方法は、以下のドキュメントを見るとよさそうです。

delve/locspec.md at v1.6.0 · go-delve/delve · GitHub

いくつか例を挙げてみます。

関数の開始行を指定する方法、ファイルの行数で指定する方法。どちらも同じ位置を指定しています。

(dlv) break debug-go-app/sub.GetMessage:0
Breakpoint 1 set at 0x4ba6b8 for debug-go-app/sub.GetMessage() ./sub/message.go:5


(dlv) break sub/message.go:5
Breakpoint 1 set at 0x4ba6b8 for debug-go-app/sub.GetMessage() ./sub/message.go:5

ブレークポイントを付けたら、nextまたはnで進めます。

(dlv) next
> debug-go-app/sub.GetMessage() ./sub/message.go:5 (hits goroutine(1):1 total:1) (PC: 0x4ba6b8)
     1: package sub
     2: 
     3: import "fmt"
     4: 
=>   5:  func GetMessage(word string) string {
     6:     return fmt.Sprintf("Hello, %s!!", word)
     7: }

次の行へ。

(dlv) n
> debug-go-app/sub.GetMessage() ./sub/message.go:6 (PC: 0x4ba6da)
     1: package sub
     2: 
     3: import "fmt"
     4: 
     5: func GetMessage(word string) string {
=>   6:      return fmt.Sprintf("Hello, %s!!", word)
     7: }

printまたはpで、変数の内容を表示。

(dlv) p word
"World"


(dlv) print word
"World"

stackまたはbtスタックトレースの表示。

(dlv) stack
0  0x00000000004ba6da in debug-go-app/sub.GetMessage
   at ./sub/message.go:6
1  0x00000000004ba82b in main.main
   at ./main.go:9
2  0x000000000043b40f in runtime.main
   at /usr/lib/go-1.15/src/runtime/proc.go:204
3  0x000000000046d8c1 in runtime.goexit
   at /usr/lib/go-1.15/src/runtime/asm_amd64.s:1374


(dlv) bt
0  0x00000000004ba6da in debug-go-app/sub.GetMessage
   at ./sub/message.go:6
1  0x00000000004ba82b in main.main
   at ./main.go:9
2  0x000000000043b40f in runtime.main
   at /usr/lib/go-1.15/src/runtime/proc.go:204
3  0x000000000046d8c1 in runtime.goexit
   at /usr/lib/go-1.15/src/runtime/asm_amd64.s:1374

少し進めてみましょう。

(dlv) n
> main.main() ./main.go:9 (PC: 0x4ba82b)
Values returned:
    ~r1: "Hello, World!!"

     4:     "debug-go-app/sub"
     5:     "fmt"
     6: )
     7: 
     8: func main() {
=>   9:      message := sub.GetMessage("World")
    10: 
    11:     fmt.Println(message)
    12: 
    13:     languages := []string{"Java", "Go", "Python", "Perl"}
    14: 
(dlv) n
> main.main() ./main.go:11 (PC: 0x4ba83f)
     6: )
     7: 
     8: func main() {
     9:     message := sub.GetMessage("World")
    10: 
=>  11:      fmt.Println(message)
    12: 
    13:     languages := []string{"Java", "Go", "Python", "Perl"}
    14: 
    15:     for _, language := range languages {
    16:         m := sub.GetMessage(language)

continueまたはcで、一気に進めることもできます。この場合、次のブレークポイントで停止します(ブレークポイントがなければ
プログラムが終了します)。

(dlv) c
Hello, World!!
> debug-go-app/sub.GetMessage() ./sub/message.go:5 (hits goroutine(1):2 total:2) (PC: 0x4ba6b8)
     1: package sub
     2: 
     3: import "fmt"
     4: 
=>   5:  func GetMessage(word string) string {
     6:     return fmt.Sprintf("Hello, %s!!", word)
     7: }


(dlv) continue
Hello, Java!!
> debug-go-app/sub.GetMessage() ./sub/message.go:5 (hits goroutine(1):3 total:3) (PC: 0x4ba6b8)
     1: package sub
     2: 
     3: import "fmt"
     4: 
=>   5:  func GetMessage(word string) string {
     6:     return fmt.Sprintf("Hello, %s!!", word)
     7: }

現在つけているブレークポイントは、breakpointsで確認できます。…なんかエラー出てますが。

(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x438d60 for runtime.fatalthrow() /usr/lib/go-1.15/src/runtime/panic.go:1162 (0)
Breakpoint unrecovered-panic at 0x438de0 for runtime.fatalpanic() /usr/lib/go-1.15/src/runtime/panic.go:1189 (0)
    print runtime.curg._panic.arg
Breakpoint 1 at 0x4ba6b8 for debug-go-app/sub.GetMessage() ./sub/message.go:5 (3)

ブレークポイントを削除するには、clearブレークポイントの番号を指定します。

(dlv) clear 1
Breakpoint 1 cleared at 0x4ba6b8 for debug-go-app/sub.GetMessage() ./sub/message.go:5

ブレークポイントがなくなったので、continueで最後まで実行して終了。

(dlv) c
Hello, Go!!
Hello, Python!!
Hello, Perl!!
Process 16021 has exited with status 0

ブレークポイントを関数につける場合は、行番号は省略できます。main.mainにつけてみましょう。

$ dlv debug main.go 
Type 'help' for list of commands.
(dlv) break main.main
Breakpoint 1 set at 0x4ba7fb for main.main() ./main.go:8

ステップ実行。

(dlv) n
> main.main() ./main.go:8 (hits goroutine(1):1 total:1) (PC: 0x4ba7fb)
     3: import (
     4:     "debug-go-app/sub"
     5:     "fmt"
     6: )
     7: 
=>   8:  func main() {
     9:     message := sub.GetMessage("World")
    10: 
    11:     fmt.Println(message)
    12: 
    13:     languages := []string{"Java", "Go", "Python", "Perl"}
(dlv) n
> main.main() ./main.go:9 (PC: 0x4ba812)
     4:     "debug-go-app/sub"
     5:     "fmt"
     6: )
     7: 
     8: func main() {
=>   9:      message := sub.GetMessage("World")
    10: 
    11:     fmt.Println(message)
    12: 
    13:     languages := []string{"Java", "Go", "Python", "Perl"}
    14:

また、stepで関数内にステップインすることができます。

(dlv) step
> debug-go-app/sub.GetMessage() ./sub/message.go:5 (PC: 0x4ba6b8)
     1: package sub
     2: 
     3: import "fmt"
     4: 
=>   5:  func GetMessage(word string) string {
     6:     return fmt.Sprintf("Hello, %s!!", word)
     7: }

関数から呼び出し元に戻るには、stepoutで。

(dlv) stepout
> main.main() ./main.go:9 (PC: 0x4ba82b)
Values returned:
    ~r1: "Hello, World!!"

     4:     "debug-go-app/sub"
     5:     "fmt"
     6: )
     7: 
     8: func main() {
=>   9:      message := sub.GetMessage("World")
    10: 
    11:     fmt.Println(message)
    12: 
    13:     languages := []string{"Java", "Go", "Python", "Perl"}
    14: 

ブレークポイントを削除して終了。

(dlv) clear 1
Breakpoint 1 cleared at 0x4ba7fb for main.main() ./main.go:8
(dlv) c
Hello, World!!
Hello, Java!!
Hello, Go!!
Hello, Python!!
Hello, Perl!!
Process 17217 has exited with status 0

execも試してみましょう。

delve/dlv_exec.md at v1.6.0 · go-delve/delve · GitHub

説明を見ると、こちらもgdbの時と同じくビルド時に最適化を無効にした方がよいみたいです。

$ go build -gcflags="all=-N -l"

ビルド済みのバイナリを指定して、dlv exec。起動後は、debugと変わりません。

$ dlv exec ./debug-go-app 
Type 'help' for list of commands.
(dlv) break debug-go-app/sub.GetMessage
Breakpoint 1 set at 0x4ba6b8 for debug-go-app/sub.GetMessage() ./sub/message.go:5
(dlv) n
> debug-go-app/sub.GetMessage() ./sub/message.go:5 (hits goroutine(1):1 total:1) (PC: 0x4ba6b8)
     1: package sub
     2: 
     3: import "fmt"
     4: 
=>   5:  func GetMessage(word string) string {
     6:     return fmt.Sprintf("Hello, %s!!", word)
     7: }

他のGoアプリケーションでも試せないでしょうか?Terraformを使ってみましょう。

$ terraform version
Terraform v0.14.6


$ which terraform
/usr/local/bin/terraform

Terraformに対して、dlv execしてみます。

$ dlv exec /usr/local/bin/terraform version
Type 'help' for list of commands.
(dlv) 

デバッグセッションが始まり、ブレークポイントはつけられるものの、関数の内容などが表示されません。

(dlv) break main.main
Breakpoint 1 set at 0x221fbaf for main.main() /home/circleci/project/project/main.go:39


(dlv) n
> main.main() /home/circleci/project/project/main.go:39 (hits goroutine(1):1 total:1) (PC: 0x221fbaf)
Warning: debugging optimized function


(dlv) n
> main.main() /home/circleci/project/project/main.go:41 (PC: 0x221fbbd)
Warning: debugging optimized function

これだと、ちょっとデバッグは厳しいですね…。

試しに、自分で書いたGoアプリケーションでビルド時のオプションを指定せずにビルドしてみましょう。

$ go build

特に問題なくデバッグできました。

$ dlv exec ./debug-go-app 
Type 'help' for list of commands.
(dlv) break main.main
Breakpoint 1 set at 0x49ab78 for main.main() ./main.go:8
(dlv) n
> main.main() ./main.go:8 (hits goroutine(1):1 total:1) (PC: 0x49ab78)
Warning: debugging optimized function
     3: import (
     4:     "debug-go-app/sub"
     5:     "fmt"
     6: )
     7: 
=>   8:  func main() {
     9:     message := sub.GetMessage("World")
    10: 
    11:     fmt.Println(message)
    12: 
    13:     languages := []string{"Java", "Go", "Python", "Perl"}

ということは、Terraformはビルド時に最適化していそうですね。

terraform/build.sh at v0.14.6 · hashicorp/terraform · GitHub

同じオプションを指定すると、見事にデバッグできなくなりました。

$ go build -ldflags='-s -w'
$ dlv exec ./debug-go-app
could not launch process: could not open debug info

ちょっとTerraformの時とは動きが違いますが…。

まあ、ビルド済みのGoアプリケーションがいつもデバッグできるとは思わない方がいい、ということですね。

エディタのプラグインもあるようです。

delve/EditorIntegration.md at v1.6.0 · go-delve/delve · GitHub

単独のUIとしては、こちら。

GitHub - aarzilli/gdlv: GUI frontend for Delve

まとめ

Goアプリケーションを、gdbおよびDelveでデバッグしてみました。

慣れればデバッグできるようになるのかなぁとは思いますが、fmt.Printlnあたりでデバッグしていることが自分は多そうな気がします。

まあ、覚えておいて損はないでしょう。