使用 Delve 代替 Println 來調試 Go 程序
你上次嘗試去學習一種新的編程語言時什麼時候?你有沒有持之以恆,你是那些在新事物發布的第一時間就勇敢地去嘗試的一員嗎?不管怎樣,學習一種新的語言也許非常有用,也會有很多樂趣。
你嘗試著寫簡單的 「Hello, world!」,然後寫一些示例代碼並執行,繼續做一些小的修改,之後繼續前進。我敢保證我們都有過這個經歷,不論我們使用哪種技術。假如你嘗試用一段時間一種語言,並且你希望能夠精通它,那麼有一些事物能在你的進取之路上幫助你。
其中之一就是調試器。有些人喜歡在代碼中用簡單的 「print」 語句進行調試,這種方式很適合代碼量少的簡單程序;然而,如果你處理的是有多個開發者和幾千行代碼的大型項目,你應該使用調試器。
最近我開始學習 Go 編程語言了,在本文中,我們將探討一種名為 Delve 的調試器。Delve 是專門用來調試 Go 程序的工具,我們會藉助一些 Go 示例代碼來了解下它的一些功能。不要擔心這裡展示的 Go 示例代碼;即使你之前沒有寫過 Go 代碼也能看懂。Go 的目標之一是簡單,因此代碼是始終如一的,理解和解釋起來都很容易。
Delve 介紹
Delve 是託管在 GitHub 上的一個開源項目。
它自己的文檔中寫道:
Delve 是 Go 編程語言的調試器。該項目的目標是為 Go 提供一個簡單、全功能的調試工具。Delve 應該是易於調用和易於使用的。當你使用調試器時,事情可能不會按你的思路運行。如果你這樣想,那麼你不適合用 Delve。
讓我們來近距離看一下。
我的測試系統是運行著 Fedora Linux 的筆記本電腦,Go 編譯器版本如下:
$ cat /etc/fedora-release
Fedora release 30 (Thirty)
$
$ go version
go version go1.12.17 linux/amd64
$
Golang 安裝
如果你沒有安裝 Go,你可以運行下面的命令,很輕鬆地就可以從配置的倉庫中獲取。
$ dnf install golang.x86_64
或者,你可以在安裝頁面找到適合你的操作系統的其他安裝版本。
在開始之前,請先確認已經設置好了 Go 工具依賴的下列各個路徑。如果這些路徑沒有設置,有些示例可能不能正常運行。你可以在 SHELL 的 RC 文件中輕鬆設置這些環境變數,我的機器上是在 $HOME/bashrc
文件中設置的。
$ go env | grep GOPATH
GOPATH="/home/user/go"
$
$ go env | grep GOBIN
GOBIN="/home/user/go/gobin"
$
Delve 安裝
你可以像下面那樣,通過運行一個簡單的 go get
命令來安裝 Delve。go get
是 Golang 從外部源下載和安裝需要的包的方式。如果你安裝過程中遇到了問題,可以查看 Delve 安裝教程。
$ go get -u github.com/go-delve/delve/cmd/dlv
$
運行上面的命令,就會把 Delve 下載到你的 $GOPATH
的位置,如果你沒有把 $GOPATH
設置成其他值,那麼默認情況下 $GOPATH
和 $HOME/go
是同一個路徑。
你可以進入 go/
目錄,你可以在 bin/
目錄下看到 dlv
。
$ ls -l $HOME/go
total 8
drwxrwxr-x. 2 user user 4096 May 25 19:11 bin
drwxrwxr-x. 4 user user 4096 May 25 19:21 src
$
$ ls -l ~/go/bin/
total 19596
-rwxrwxr-x. 1 user user 20062654 May 25 19:17 dlv
$
因為你把 Delve 安裝到了 $GOPATH
,所以你可以像運行普通的 shell 命令一樣運行它,即每次運行時你不必先進入它所在的目錄。你可以通過 version
選項來驗證 dlv
是否正確安裝。示例中安裝的版本是 1.4.1。
$ which dlv
~/go/bin/dlv
$
$ dlv version
Delve Debugger
Version: 1.4.1
Build: $Id: bda606147ff48b58bde39e20b9e11378eaa4db46 $
$
現在,我們一起在 Go 程序中使用 Delve 來理解下它的功能以及如何使用它們。我們先來寫一個 hello.go
,簡單地列印一條 Hello, world!
信息。
記著,我把這些示常式序放到了 $GOBIN
目錄下。
$ pwd
/home/user/go/gobin
$
$ cat hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
$
運行 build
命令來編譯一個 Go 程序,它的輸入是 .go
後綴的文件。如果程序沒有語法錯誤,Go 編譯器把它編譯成一個二進位可執行文件。這個文件可以被直接運行,運行後我們會在屏幕上看到 Hello, world!
信息。
$ go build hello.go
$
$ ls -l hello
-rwxrwxr-x. 1 user user 1997284 May 26 12:13 hello
$
$ ./hello
Hello, world!
$
在 Delve 中載入程序
把一個程序載入進 Delve 調試器有兩種方式。
在源碼編譯成二進位文件之前使用 debug 參數
第一種方式是在需要時對源碼使用 debug
命令。Delve 會為你編譯出一個名為 __debug_bin
的二進位文件,並把它載入進調試器。
在這個例子中,你可以進入 hello.go
所在的目錄,然後運行 dlv debug
命令。如果目錄中有多個源文件且每個文件都有自己的主函數,Delve 則可能拋出錯誤,它期望的是單個程序或從單個項目構建成單個二進位文件。如果出現了這種錯誤,那麼你就應該用下面展示的第二種方式。
$ ls -l
total 4
-rw-rw-r--. 1 user user 74 Jun 4 11:48 hello.go
$
$ dlv debug
Type 'help' for list of commands.
(dlv)
現在打開另一個終端,列出目錄下的文件。你可以看到一個多出來的 __debug_bin
二進位文件,這個文件是由源碼編譯生成的,並會載入進調試器。你現在可以回到 dlv
提示框繼續使用 Delve。
$ ls -l
total 2036
-rwxrwxr-x. 1 user user 2077085 Jun 4 11:48 __debug_bin
-rw-rw-r--. 1 user user 74 Jun 4 11:48 hello.go
$
使用 exec 參數
如果你已經有提前編譯好的 Go 程序或者已經用 go build
命令編譯完成了,不想再用 Delve 編譯出 __debug_bin
二進位文件,那麼第二種把程序載入進 Delve 的方法在這些情況下會很有用。在上述情況下,你可以使用 exec
命令來把整個目錄載入進 Delve 調試器。
$ ls -l
total 4
-rw-rw-r--. 1 user user 74 Jun 4 11:48 hello.go
$
$ go build hello.go
$
$ ls -l
total 1956
-rwxrwxr-x. 1 user user 1997284 Jun 4 11:54 hello
-rw-rw-r--. 1 user user 74 Jun 4 11:48 hello.go
$
$ dlv exec ./hello
Type 'help' for list of commands.
(dlv)
查看 delve 幫助信息
在 dlv
提示符中,你可以運行 help
來查看 Delve 提供的多種幫助選項。命令列表相當長,這裡我們只列舉一些重要的功能。下面是 Delve 的功能概覽。
(dlv) help
The following commands are available:
Running the program:
Manipulating breakpoints:
Viewing program variables and memory:
Listing and switching between threads and goroutines:
Viewing the call stack and selecting frames:
Other commands:
Type help followed by a command for full documentation.
(dlv)
設置斷點
現在我們已經把 hello.go 程序載入進了 Delve 調試器,我們在主函數處設置斷點,稍後來確認它。在 Go 中,主程序從 main.main
處開始執行,因此你需要給這個名字提供個 break
命令。之後,我們可以用 breakpoints
命令來檢查斷點是否正確設置了。
不要忘了你還可以用命令簡寫,因此你可以用 b main.main
來代替 break main.main
,兩者效果相同,bp
和 breakpoints
同理。你可以通過運行 help
命令查看幫助信息來找到你想要的命令簡寫。
(dlv) break main.main
Breakpoint 1 set at 0x4a228f for main.main() ./hello.go:5
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x42c410 for runtime.fatalthrow() /usr/lib/golang/src/runtime/panic.go:663 (0)
Breakpoint unrecovered-panic at 0x42c480 for runtime.fatalpanic() /usr/lib/golang/src/runtime/panic.go:690 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x4a228f for main.main() ./hello.go:5 (0)
(dlv)
程序繼續執行
現在,我們用 continue
來繼續運行程序。它會運行到斷點處中止,在我們的例子中,會運行到主函數的 main.main
處中止。從這裡開始,我們可以用 next
命令來逐行執行程序。請注意,當我們運行到 fmt.Println("Hello, world!")
處時,即使我們還在調試器里,我們也能看到列印到屏幕的 Hello, world!
。
(dlv) continue
> main.main() ./hello.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a228f)
1: package main
2:
3: import "fmt"
4:
=> 5: func main() {
6: fmt.Println("Hello, world!")
7: }
(dlv) next
> main.main() ./hello.go:6 (PC: 0x4a229d)
1: package main
2:
3: import "fmt"
4:
5: func main() {
=> 6: fmt.Println("Hello, world!")
7: }
(dlv) next
Hello, world!
> main.main() ./hello.go:7 (PC: 0x4a22ff)
2:
3: import "fmt"
4:
5: func main() {
6: fmt.Println("Hello, world!")
=> 7: }
(dlv)
退出 Delve
你隨時可以運行 quit
命令來退出調試器,退出之後你會回到 shell 提示符。相當簡單,對嗎?
(dlv) quit
$
Delve 的其他功能
我們用其他的 Go 程序來探索下 Delve 的其他功能。這次,我們從 golang 教程 中找了一個程序。如果你要學習 Go 語言,那麼 Golang 教程應該是你的第一站。
下面的程序,functions.go
中簡單展示了 Go 程序中是怎樣定義和調用函數的。這裡,我們有一個簡單的把兩數相加並返回和值的 add()
函數。你可以像下面那樣構建程序並運行它。
$ cat functions.go
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
$
你可以像下面那樣構建和運行程序。
$ go build functions.go && ./functions
55
$
進入函數
跟前面展示的一樣,我們用前面提到的一個選項來把二進位文件載入進 Delve 調試器,再一次在 main.main
處設置斷點,繼續運行程序直到斷點處。然後執行 next
直到 fmt.Println(add(42, 13))
處;這裡我們調用了 add()
函數。我們可以像下面展示的那樣,用 Delve 的 step
命令從 main
函數進入 add()
函數。
$ dlv debug
Type 'help' for list of commands.
(dlv) break main.main
Breakpoint 1 set at 0x4a22b3 for main.main() ./functions.go:9
(dlv) c
> main.main() ./functions.go:9 (hits goroutine(1):1 total:1) (PC: 0x4a22b3)
4:
5: func add(x int, y int) int {
6: return x + y
7: }
8:
=> 9: func main() {
10: fmt.Println(add(42, 13))
11: }
(dlv) next
> main.main() ./functions.go:10 (PC: 0x4a22c1)
5: func add(x int, y int) int {
6: return x + y
7: }
8:
9: func main() {
=> 10: fmt.Println(add(42, 13))
11: }
(dlv) step
> main.add() ./functions.go:5 (PC: 0x4a2280)
1: package main
2:
3: import "fmt"
4:
=> 5: func add(x int, y int) int {
6: return x + y
7: }
8:
9: func main() {
10: fmt.Println(add(42, 13))
(dlv)
使用文件名:行號
來設置斷點
上面的例子中,我們經過 main
函數進入了 add()
函數,但是你也可以在你想加斷點的地方直接使用「文件名:行號」的組合。下面是在 add()
函數開始處加斷點的另一種方式。
(dlv) break functions.go:5
Breakpoint 1 set at 0x4a2280 for main.add() ./functions.go:5
(dlv) continue
> main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280)
1: package main
2:
3: import "fmt"
4:
=> 5: func add(x int, y int) int {
6: return x + y
7: }
8:
9: func main() {
10: fmt.Println(add(42, 13))
(dlv)
查看當前的棧信息
現在我們運行到了 add()
函數,我們可以在 Delve 中用 stack
命令查看當前棧的內容。這裡在 0
位置展示了棧頂的函數 add()
,緊接著在 1
位置展示了調用 add()
函數的 main.main
。在 main.main
下面的函數屬於 Go 運行時,是用來處理載入和執行該程序的。
(dlv) stack
0 0x00000000004a2280 in main.add
at ./functions.go:5
1 0x00000000004a22d7 in main.main
at ./functions.go:10
2 0x000000000042dd1f in runtime.main
at /usr/lib/golang/src/runtime/proc.go:200
3 0x0000000000458171 in runtime.goexit
at /usr/lib/golang/src/runtime/asm_amd64.s:1337
(dlv)
在幀之間跳轉
在 Delve 中我們可以用 frame
命令實現幀之間的跳轉。在下面的例子中,我們用 frame
實現了從 add()
幀跳到 main.main
幀,以此類推。
(dlv) frame 0
> main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280)
Frame 0: ./functions.go:5 (PC: 4a2280)
1: package main
2:
3: import "fmt"
4:
=> 5: func add(x int, y int) int {
6: return x + y
7: }
8:
9: func main() {
10: fmt.Println(add(42, 13))
(dlv) frame 1
> main.add() ./functions.go:5 (hits goroutine(1):1 total:1) (PC: 0x4a2280)
Frame 1: ./functions.go:10 (PC: 4a22d7)
5: func add(x int, y int) int {
6: return x + y
7: }
8:
9: func main() {
=> 10: fmt.Println(add(42, 13))
11: }
(dlv)
列印函數參數
一個函數通常會接收多個參數。在 add()
函數中,它的入參是兩個整型。Delve 有個便捷的 args
命令,它能列印出命令行傳給函數的參數。
(dlv) args
x = 42
y = 13
~r2 = 824633786832
(dlv)
查看反彙編碼
由於我們是調試編譯出的二進位文件,因此如果我們能查看編譯器生成的彙編語言指令將會非常有用。Delve 提供了一個 disassemble
命令來查看這些指令。在下面的例子中,我們用它來查看 add()
函數的彙編指令。
(dlv) step
> main.add() ./functions.go:5 (PC: 0x4a2280)
1: package main
2:
3: import "fmt"
4:
=> 5: func add(x int, y int) int {
6: return x + y
7: }
8:
9: func main() {
10: fmt.Println(add(42, 13))
(dlv) disassemble
TEXT main.add(SB) /home/user/go/gobin/functions.go
=> functions.go:5 0x4a2280 48c744241800000000 mov qword ptr [rsp+0x18], 0x0
functions.go:6 0x4a2289 488b442408 mov rax, qword ptr [rsp+0x8]
functions.go:6 0x4a228e 4803442410 add rax, qword ptr [rsp+0x10]
functions.go:6 0x4a2293 4889442418 mov qword ptr [rsp+0x18], rax
functions.go:6 0x4a2298 c3 ret
(dlv)
單步退出函數
另一個功能是 stepout
,這個功能可以讓我們跳回到函數被調用的地方。在我們的例子中,如果我們想回到 main.main
函數,我們只需要簡單地運行 stepout
命令,它就會把我們帶回去。在我們調試大型代碼庫時,這個功能會是一個非常便捷的工具。
(dlv) stepout
> main.main() ./functions.go:10 (PC: 0x4a22d7)
Values returned:
~r2: 55
5: func add(x int, y int) int {
6: return x + y
7: }
8:
9: func main() {
=> 10: fmt.Println(add(42, 13))
11: }
(dlv)
列印變數信息
我們一起通過 Go 教程 的另一個示常式序來看下 Delve 是怎麼處理 Go 中的變數的。下面的示常式序定義和初始化了一些不同類型的變數。你可以構建和運行程序。
$ cat variables.go
package main
import "fmt"
var i, j int = 1, 2
func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
$
$ go build variables.go &&; ./variables
1 2 true false no!
$
像前面說過的那樣,用 delve debug
在調試器中載入程序。你可以在 Delve 中用 print
命令通過變數名來展示他們當前的值。
(dlv) print c
true
(dlv) print java
"no!"
(dlv)
或者,你還可以用 locals
命令來列印函數內所有的局部變數。
(dlv) locals
python = false
c = true
java = "no!"
(dlv)
如果你不知道變數的類型,你可以用 whatis
命令來通過變數名來列印它的類型。
(dlv) whatis python
bool
(dlv) whatis c
bool
(dlv) whatis java
string
(dlv)
總結
現在我們只是了解了 Delve 所有功能的皮毛。你可以自己去查看幫助內容,嘗試下其它的命令。你還可以把 Delve 綁定到運行中的 Go 程序上(守護進程!),如果你安裝了 Go 源碼庫,你甚至可以用 Delve 導出 Golang 庫內部的信息。勇敢去探索吧!
via: https://opensource.com/article/20/6/debug-go-delve
作者:Gaurav Kamathe 選題:lujun9972 譯者:lxbwolf 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive