一個 Demo 學會使用 Go Delve 調試
本文轉載自微信公眾號「腦子進煎魚了」,作者陳煎魚。轉載本文請聯系腦子進煎魚了公眾號。
大家好,我是煎魚。
在 Go 語言中,除了 go tool 工具鏈中的 pprof、trace 等剖析工具的大利器外。常常還會有小伙伴問,有沒有更好用,更精細的,
大家總嫌棄 pprof、trace 等工具,不夠細,沒法一口氣看到根因,或者具體變量...希望能夠最好能追到代碼級別調試的,看到具體變量的值是怎么樣的,隨意想怎么看怎么看的那種。
為此今天給大家介紹 Go 語言強大的 Delve (dlv)調試工具,來更深入問題剖析。
安裝
我們需要先安裝 Go delve,若是 Go1.16 及以后的版本,可以直接執行下述命令安裝:
- $ go install github.com/go-delve/delve/cmd/dlv@latest
也可以通過 git clone 的方式安裝:
- $ git clone https://github.com/go-delve/delve
- $ cd delve
- $ go install github.com/go-delve/delve/cmd/dlv
在安裝完畢后,我們執行 dlv version 命令,查看安裝情況:
- $ dlv version
- Delve Debugger
- Version: 1.7.0
- Build: $Id: e353a65161e6ed74952b96bbb62ebfc56090832b $
可以明確看到我們所安裝的版本是 v1.7.0。
演示程序
我們計劃用一個反轉字符串的演示程序來進行 Go 程序的調試。第一部分先是完成 stringer 包的 Reverse 方法。
代碼如下:
- package stringer
- func Reverse(s string) string {
- r := []rune(s)
- for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
- r[i], r[j] = r[j], r[i]
- }
- return string(r)
- }
再在具體的 main 啟動函數中進行調用。代碼如下:
- import (
- "fmt"
- "github.com/eddycjy/awesome-project/stringer"
- )
- func main() {
- fmt.Println(stringer.Reverse("腦子進煎魚了!"))
- }
輸出結果:
- !了魚煎進子腦
進行調試
Delve 是 Go 程序的源代碼級調試器。Delve 使您能夠通過控制流程的執行與您的程序進行交互,查看變量,提供線程、goroutine、CPU 狀態等信息。
其一共支持如下 11 個子命令:
- Available Commands:
- attach Attach to running process and begin debugging.
- connect Connect to a headless debug server.
- core Examine a core dump.
- dap [EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
- debug Compile and begin debugging main package in current directory, or the package specified.
- exec Execute a precompiled binary, and begin a debug session.
- help Help about any command
- run Deprecated command. Use 'debug' instead.
- test Compile test binary and begin debugging program.
- trace Compile and begin tracing program.
- version Prints version.
我們今天主要用到的是 debug 命令,他能夠編譯并開始調試當前目錄下的主包,或指定的包,是最常用的功能之一。
接下來我們利用這個演示程序來進行 dlv 的深入調試和應用。
執行如下命令:
- ➜ awesomeProject dlv debug .
- Type 'help' for list of commands.
- (dlv)
我們先在演示程序根目錄下執行了 debug,進入了 dlv 的交互模式。
再使用關鍵字 b(break 的縮寫)對 main.main 方法設置斷點:
- (dlv) b main.main
- Breakpoint 1 (enabled) set at 0x10cbab3 for main.main() ./main.go:9
- (dlv)
設置完畢后,我們可以看到方法對應的文件名、行數。接著我們可以執行關鍵字 c(continue 的縮寫)跳轉到下一個斷點處:
- (dlv) c
- > main.main() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x10cbab3)
- 4: "fmt"
- 5:
- 6: "github.com/eddycjy/awesome-project/stringer"
- 7: )
- 8:
- => 9: func main() {
- 10: fmt.Println(stringer.Reverse("腦子進煎魚了!"))
- 11: }
- (dlv)
在斷點處,我看可以看到具體的代碼塊、goroutine、CPU 寄存器地址等運行時信息。
緊接著執行關鍵字 n(next 的縮寫)單步執行程序的下一步:
- (dlv) n
- > main.main() ./main.go:10 (PC: 0x10cbac1)
- 5:
- 6: "github.com/eddycjy/awesome-project/stringer"
- 7: )
- 8:
- 9: func main() {
- => 10: fmt.Println(stringer.Reverse("腦子進煎魚了!"))
- 11: }
我們可以看到程序走到了 main.go 文件中的第 10 行中,并且調用了 stringer.Reverse 方法去處理。
此時我們可以執行關鍵字 s(step 的關鍵字)進入到這個函數中去繼續調試:
- (dlv) s
- > github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3 (PC: 0x10cb87b)
- 1: package stringer
- 2:
- => 3: func Reverse(s string) string {
- 4: r := []rune(s)
- 5: for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
- 6: r[i], r[j] = r[j], r[i]
- 7: }
- 8: return string(r)
輸入后,調試的光標會到 Reverse 方法上,此時我們可以調用關鍵字 p(print 的縮寫)傳出所傳入的變量的值:
- (dlv) p s
- "腦子進煎魚了!"
此處函數的形參變量是 s,輸出了 “腦子進煎魚了!”,與我們所傳入的是一致的。
但故事一般沒有這么的簡單,會用到 Delve 來調試,說明是比較細致、隱患的 BUG。為此我們大多需要更進一步的深入。
我們繼續圍觀 Reverse 方法:
- 5: for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
- 6: r[i], r[j] = r[j], r[i]
- 7: }
從表現來看,我們常常會懷疑是第 6 行可能是問題的所在。這時可以針對性的對第 6 行進行斷點查看:
- (dlv) b 6
- Breakpoint 2 (enabled) set at 0x10cb92c for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6
設置完斷點后,我們只需要執行關鍵字 c,繼續下一步:
- (dlv) c
- > github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6 (hits goroutine(1):1 total:1) (PC: 0x10cb92c)
- 1: package stringer
- 2:
- 3: func Reverse(s string) string {
- 4: r := []rune(s)
- 5: for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
- => 6: r[i], r[j] = r[j], r[i]
- 7: }
- 8: return string(r)
- 9: }
走到對應的代碼片段后,執行關鍵字 locals:
- (dlv) locals
- r = []int32 len: 7, cap: 32, [...]
- j = 6
- i = 0
我們就可以看到對應的變量 r, i, j 的值是多少,可以根據此來分析程序流轉是否與我們預想的一致。
另外也可以調用關鍵字 set 去針對特定變量設置期望的值:
- (dlv) set i = 1
- (dlv) locals
- r = []int32 len: 7, cap: 32, [...]
- j = 6
- i = 1
設置后,若還需要繼續排查,可以繼續調用關鍵字 c 去定位,這種常用于特定變量的特定值的異常,這樣一設置一調試基本就能排查出來了。
在排查完畢后,我們可以執行關鍵字 r(reset 的縮寫):
- (dlv) r
- Process restarted with PID 56614
執行完畢后,整個調試就會重置,像是前面在打斷點時所設置的變量值就會恢復。
若要查看設置的斷點情況,也可以執行關鍵字 bp 查看:
- (dlv) bp
- Breakpoint runtime-fatal-throw (enabled) at 0x1038fc0 for runtime.fatalthrow() /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1163 (0)
- Breakpoint unrecovered-panic (enabled) at 0x1039040 for runtime.fatalpanic() /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1190 (0)
- print runtime.curg._panic.arg
- Breakpoint 1 (enabled) at 0x10cbab3 for main.main() ./main.go:9 (0)
- Breakpoint 2 (enabled) at 0x10cb92c for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6 (0)
查看斷點情況后,若有部分已經排除了,可以調用關鍵字 clearall 對一些斷點清除:
- (dlv) clearall main.main
- Breakpoint 1 (enabled) cleared at 0x10cbab3 for main.main() ./main.go:9
若不指點斷點,則會默認清除全部斷點。
在日常的 Go 工程中,若都從 main 方法進入就太繁瑣了。我們可以直接借助函數名進行調式定位:
- (dlv) funcs Reverse
- github.com/eddycjy/awesome-project/stringer.Reverse
- (dlv) b stringer.Reverse
- Breakpoint 3 (enabled) set at 0x10cb87b for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3
- (dlv) c
- > github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3 (hits goroutine(1):1 total:1) (PC: 0x10cb87b)
- 1: package stringer
- 2:
- => 3: func Reverse(s string) string {
- 4: r := []rune(s)
- 5: for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
- 6: r[i], r[j] = r[j], r[i]
- 7: }
- 8: return string(r)
緊接著其他步驟都與先前的一樣,進行具體的調試就好了。我們也可以借助 Go 語言的公共函數進行計算:
- (dlv) p len(r)-1
- 6
也可以借助關鍵字 vars 查看某個包下的所有全局變量的值,例如:vars main。這種方式對于查看全局變量的情況非常有幫助。
排查完畢后,執行關鍵字 exit 就可以愉快的退出了:
- (dlv) exit
解決完問題,可以下班了 :)
總結
在 Go 語言中,Delve 調試工具是與 Go 語言親和度最高的,因為 Delve 是 Go 語言實現的。其在我們日常工作中,非常常用。
像是假設程序的 for 循環運行到第 N 次才出現 BUG 時,我們就可以通過斷點對應的方法和代碼塊,再設置變量的值,進行具體的查看,就可以解決。