一個 Demo 學(xué)會使用 Go Delve 調(diào)試
本文轉(zhuǎn)載自微信公眾號「腦子進(jìn)煎魚了」,作者陳煎魚。轉(zhuǎn)載本文請聯(lián)系腦子進(jìn)煎魚了公眾號。
大家好,我是煎魚。
在 Go 語言中,除了 go tool 工具鏈中的 pprof、trace 等剖析工具的大利器外。常常還會有小伙伴問,有沒有更好用,更精細(xì)的,
大家總嫌棄 pprof、trace 等工具,不夠細(xì),沒法一口氣看到根因,或者具體變量...希望能夠最好能追到代碼級別調(diào)試的,看到具體變量的值是怎么樣的,隨意想怎么看怎么看的那種。
為此今天給大家介紹 Go 語言強(qiáng)大的 Delve (dlv)調(diào)試工具,來更深入問題剖析。
安裝
我們需要先安裝 Go delve,若是 Go1.16 及以后的版本,可以直接執(zhí)行下述命令安裝:
- $ 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
在安裝完畢后,我們執(zhí)行 dlv version 命令,查看安裝情況:
- $ dlv version
- Delve Debugger
- Version: 1.7.0
- Build: $Id: e353a65161e6ed74952b96bbb62ebfc56090832b $
可以明確看到我們所安裝的版本是 v1.7.0。
演示程序
我們計劃用一個反轉(zhuǎn)字符串的演示程序來進(jìn)行 Go 程序的調(diào)試。第一部分先是完成 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 啟動函數(shù)中進(jìn)行調(diào)用。代碼如下:
- import (
- "fmt"
- "github.com/eddycjy/awesome-project/stringer"
- )
- func main() {
- fmt.Println(stringer.Reverse("腦子進(jìn)煎魚了!"))
- }
輸出結(jié)果:
- !了魚煎進(jìn)子腦
進(jìn)行調(diào)試
Delve 是 Go 程序的源代碼級調(diào)試器。Delve 使您能夠通過控制流程的執(zhí)行與您的程序進(jìn)行交互,查看變量,提供線程、goroutine、CPU 狀態(tài)等信息。
其一共支持如下 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 命令,他能夠編譯并開始調(diào)試當(dāng)前目錄下的主包,或指定的包,是最常用的功能之一。
接下來我們利用這個演示程序來進(jìn)行 dlv 的深入調(diào)試和應(yīng)用。
執(zhí)行如下命令:
- ➜ awesomeProject dlv debug .
- Type 'help' for list of commands.
- (dlv)
我們先在演示程序根目錄下執(zhí)行了 debug,進(jìn)入了 dlv 的交互模式。
再使用關(guān)鍵字 b(break 的縮寫)對 main.main 方法設(shè)置斷點(diǎn):
- (dlv) b main.main
- Breakpoint 1 (enabled) set at 0x10cbab3 for main.main() ./main.go:9
- (dlv)
設(shè)置完畢后,我們可以看到方法對應(yīng)的文件名、行數(shù)。接著我們可以執(zhí)行關(guān)鍵字 c(continue 的縮寫)跳轉(zhuǎn)到下一個斷點(diǎn)處:
- (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("腦子進(jìn)煎魚了!"))
- 11: }
- (dlv)
在斷點(diǎn)處,我看可以看到具體的代碼塊、goroutine、CPU 寄存器地址等運(yùn)行時信息。
緊接著執(zhí)行關(guān)鍵字 n(next 的縮寫)單步執(zhí)行程序的下一步:
- (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("腦子進(jìn)煎魚了!"))
- 11: }
我們可以看到程序走到了 main.go 文件中的第 10 行中,并且調(diào)用了 stringer.Reverse 方法去處理。
此時我們可以執(zhí)行關(guān)鍵字 s(step 的關(guān)鍵字)進(jìn)入到這個函數(shù)中去繼續(xù)調(diào)試:
- (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)
輸入后,調(diào)試的光標(biāo)會到 Reverse 方法上,此時我們可以調(diào)用關(guān)鍵字 p(print 的縮寫)傳出所傳入的變量的值:
- (dlv) p s
- "腦子進(jìn)煎魚了!"
此處函數(shù)的形參變量是 s,輸出了 “腦子進(jìn)煎魚了!”,與我們所傳入的是一致的。
但故事一般沒有這么的簡單,會用到 Delve 來調(diào)試,說明是比較細(xì)致、隱患的 BUG。為此我們大多需要更進(jìn)一步的深入。
我們繼續(xù)圍觀 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: }
從表現(xiàn)來看,我們常常會懷疑是第 6 行可能是問題的所在。這時可以針對性的對第 6 行進(jìn)行斷點(diǎn)查看:
- (dlv) b 6
- Breakpoint 2 (enabled) set at 0x10cb92c for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6
設(shè)置完斷點(diǎn)后,我們只需要執(zhí)行關(guān)鍵字 c,繼續(xù)下一步:
- (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: }
走到對應(yīng)的代碼片段后,執(zhí)行關(guān)鍵字 locals:
- (dlv) locals
- r = []int32 len: 7, cap: 32, [...]
- j = 6
- i = 0
我們就可以看到對應(yīng)的變量 r, i, j 的值是多少,可以根據(jù)此來分析程序流轉(zhuǎn)是否與我們預(yù)想的一致。
另外也可以調(diào)用關(guān)鍵字 set 去針對特定變量設(shè)置期望的值:
- (dlv) set i = 1
- (dlv) locals
- r = []int32 len: 7, cap: 32, [...]
- j = 6
- i = 1
設(shè)置后,若還需要繼續(xù)排查,可以繼續(xù)調(diào)用關(guān)鍵字 c 去定位,這種常用于特定變量的特定值的異常,這樣一設(shè)置一調(diào)試基本就能排查出來了。
在排查完畢后,我們可以執(zhí)行關(guān)鍵字 r(reset 的縮寫):
- (dlv) r
- Process restarted with PID 56614
執(zhí)行完畢后,整個調(diào)試就會重置,像是前面在打斷點(diǎn)時所設(shè)置的變量值就會恢復(fù)。
若要查看設(shè)置的斷點(diǎn)情況,也可以執(zhí)行關(guān)鍵字 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)
查看斷點(diǎn)情況后,若有部分已經(jīng)排除了,可以調(diào)用關(guān)鍵字 clearall 對一些斷點(diǎn)清除:
- (dlv) clearall main.main
- Breakpoint 1 (enabled) cleared at 0x10cbab3 for main.main() ./main.go:9
若不指點(diǎn)斷點(diǎn),則會默認(rèn)清除全部斷點(diǎn)。
在日常的 Go 工程中,若都從 main 方法進(jìn)入就太繁瑣了。我們可以直接借助函數(shù)名進(jìn)行調(diào)式定位:
- (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)
緊接著其他步驟都與先前的一樣,進(jìn)行具體的調(diào)試就好了。我們也可以借助 Go 語言的公共函數(shù)進(jìn)行計算:
- (dlv) p len(r)-1
- 6
也可以借助關(guān)鍵字 vars 查看某個包下的所有全局變量的值,例如:vars main。這種方式對于查看全局變量的情況非常有幫助。
排查完畢后,執(zhí)行關(guān)鍵字 exit 就可以愉快的退出了:
- (dlv) exit
解決完問題,可以下班了 :)
總結(jié)
在 Go 語言中,Delve 調(diào)試工具是與 Go 語言親和度最高的,因?yàn)?Delve 是 Go 語言實(shí)現(xiàn)的。其在我們?nèi)粘9ぷ髦?,非常常用?/p>
像是假設(shè)程序的 for 循環(huán)運(yùn)行到第 N 次才出現(xiàn) BUG 時,我們就可以通過斷點(diǎn)對應(yīng)的方法和代碼塊,再設(shè)置變量的值,進(jìn)行具體的查看,就可以解決。






