你上次尝试学习一门新的编程语言是什么时候?你是坚持使用你久经考验的语言,还是属于那种一旦有新语言发布就勇敢尝试的人?无论哪种方式,学习一门新的语言都非常有用,而且充满乐趣。
你从一个简单的 "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 工具所需的必要 PATH。如果未设置这些路径,某些示例可能无法正常工作。这些可以很容易地设置为你 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 位置,在默认情况下,该位置恰好是 $HOME/go。如果你已将 $GOPATH 设置为其他位置,则会有所不同。
你可以移动到 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
$
由于你已在 $GOPATH 下安装了 Delve,因此它也可以作为常规 shell 命令使用,因此你不必每次都移动到安装它的目录。你可以通过使用 version
选项运行它来验证 dlv
是否已正确安装。它安装的版本是 1.4.1。
$ which dlv
~/go/bin/dlv
$
$ dlv version
Delve Debugger
Version: 1.4.1
Build: $Id: bda606147ff48b58bde39e20b9e11378eaa4db46 $
$
现在,让我们将 Delve 与一些 Go 程序一起使用,以了解其功能以及如何使用它们。与所有程序一样,让我们从一个简单的 "Hello, world!" 消息开始,在 Go 中,它被称为 hello.go
。
请记住,我将这些示例程序放在 $GOBIN 目录中。
$ pwd
/home/user/go/gobin
$
$ cat hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
$
要构建 Go 程序,你需要运行 build
命令,并使用 .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 命令。如果一个目录中有多个 Go 源文件,并且每个文件都有自己的 main 函数,那么 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 参数
将程序加载到 Delve 的第二种方法在你有一个预编译的 Go 二进制文件,或者你已经使用 go build 命令编译了一个二进制文件,并且不想让 Delve 将其编译为 __debug_bin
二进制文件时很有用。在这种情况下,使用 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)
设置断点
现在我们已经在 Delve 调试器中加载了 hello.go 程序,让我们在 main 函数上设置断点,然后确认它。在 Go 中,主程序以 main.main
开头,因此你需要将此名称提供给 break command
。接下来,我们将使用 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
或 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
$
让我们使用其他一些 Go 程序来探索 Delve 的其他一些功能。这次,我们将从 Golang tour 中选择一个程序。如果你正在学习 Go,Golang tour 应该是你的第一站。
以下程序 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()
函数,但是你也可以使用 filename:linenumber
组合直接在你想要的位置设置断点。以下是在 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
命令查看堆栈的当前内容。这显示了我们当前所在的顶层函数 add()
,索引为 0,其次是从中调用 add()
函数的 main.main
,索引为 1。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 1
将我们从 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 tour 中的另一个示例程序,看看 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 提供的功能的表面。你可以参考 help
部分并尝试各种其他命令。其他一些有用的功能包括将 Delve 附加到正在运行的 Go 程序(守护进程!)甚至使用 Delve 来探索 Golang 库的一些内部结构,前提是你已安装 Go 源代码包。继续探索吧!
评论已关闭。