Linux中國

使用 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,兩者效果相同,bpbreakpoints 同理。你可以通過運行 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

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國