これは、なにをしたくて書いたもの?
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.
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アプリケーションをデバッグする
どうやら、ビルド時にオプションを付けた方がよさそうです。最適化が効いていると、うまくデバッグできない可能性があるみたいです。
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は、サブコマンドを使って実行するようです。たとえば、以下のようなサブコマンドがあります(全部は載せていません)。
- 実行中のプロセスにアタッチする
attach
- ヘッドレスデバッグサーバーに接続する
connect
- カレントディレクトリまたは指定パッケージをコンパイルして
main
パッケージのデバッグを開始するdebug
- コンパイル済みのバイナリをデバッグする
exec
- テストバイナリをコンパイルしてデバッグする
test
- コンパイルしてトレーシングを始める
trace
まずは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
あたりでデバッグしていることが自分は多そうな気がします。
まあ、覚えておいて損はないでしょう。