一個 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 時,我們就可以通過斷點對應的方法和代碼塊,再設置變量的值,進行具體的查看,就可以解決。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Yz_p0S5N4ubf8wxLm5wbmQ