对调试器的需求
在任何编程语言中,最简单的调试形式是使用打印语句/日志和写到标准输出。这无疑是可行的,但当我们的应用程序的规模越来越大,逻辑越来越复杂时,就变得非常困难。将打印语句添加到应用程序的每条代码路径中并不容易。这时,调试器就派上用场了。调试器帮助我们使用断点和其他大量的功能来追踪程序的执行路径。Delve就是这样一个用于Go的调试器。在本教程中,我们将学习如何使用Delve对Go程序进行调试。
安装Delve
请确保你在一个不包含go.mod文件的目录中。我更喜欢我的Documents 目录。
cd ~/Documents/
接下来,让我们设置GOBIN 环境变量。这个环境变量指定了安装Delve 二进制文件的位置。如果你已经设置了GOBIN ,请跳过此步骤。你可以通过运行下面的命令来检查GOBIN 是否被设置。
go env | grep GOBIN
如果上面的命令打印出:GOBIN="" ,这意味着GOBIN 没有被设置。请运行export GOBIN=~/go/bin/ 命令来设置GOBIN。
让我们通过运行GOBIN ,把PATH 。export PATH=$PATH:~/go/bin
在macOS的情况下,需要Xcode命令行开发工具来运行Delve。请运行xcode-select --install 来安装命令行工具。Linux用户可以跳过这个步骤。
现在我们准备安装Delve 。请运行
go get github.com/go-delve/delve/cmd/dlv
来安装Delve。运行这个命令后,请通过运行dlv version 来测试你的安装。它将在安装成功后打印出Delve的版本。
Delve Debugger
Version: 1.4.0
Build: $Id: 67422e6f7148fa1efa0eac1423ab5594b223d93b
启动Delve
让我们写一个简单的程序,然后用Delve开始调试它。
让我们用下面的命令为我们的样本程序创建一个目录。
mkdir ~/Documents/debugsample
在我们刚刚创建的debugsample 目录内创建一个文件main.go ,内容如下。
package main
import (
"fmt"
)
func main() {
arr := []int{101, 95, 10, 188, 100}
max := arr[0]
for _, v := range arr {
if v > max {
max = v
}
}
fmt.Printf("Max element is %d\n", max)
}
上面的程序将打印片断的最大元素arr 。运行上面的程序将输出。
Max element is 188
我们现在准备对程序进行调试。让我们移动到debugsample目录cd ~/Documents/debugsample 。之后,输入下面的命令来启动Delve。
dlv debug
上面的命令将开始调试当前目录下的main 包。输入上述命令后,你可以看到终端已经变成了(dlv) 提示。如果你能看到这个变化,说明调试器已经成功启动,正在等待我们的命令:)。
让我们启动我们的第一个命令。
在dlv 提示下,输入continue 。
(dlv) continue
continue 命令将运行程序,直到有一个断点或直到程序完成。由于我们没有定义任何断点,程序将运行到完成。
Max element is 188
Process 1733 has exited with status 0
如果你看到上面的输出,说明调试器已经运行,程序已经完成:)。但这对我们来说没有任何用处。让我们继续添加几个断点,看着调试器发挥它的魔力。
创建断点
断点会在指定的行上暂停程序的执行。当执行被暂停时,我们可以向调试器发送命令,打印变量的值,查看程序的堆栈跟踪,等等。
下面提供了创建断点的语法。
(dlv) break filename:lineno
上述命令将在文件filename 中的lineno 行创建一个断点。
让我们在第9行添加一个断点。main.go 的第9行。
(dlv) break main.go:9
当上述命令运行时,你可以看到输出,Process 1733 has exited with status 0 。实际上,这个断点并没有被添加。这是因为我们之前运行continue ,程序已经退出了,因为当时没有断点。让我们重新启动程序,再次尝试设置断点。
(dlv) restart
Process restarted with PID 2028
(dlv) break main.go:9
Breakpoint 1 set at 0x10c16e4 for main.main() ./main.go:9
restart 命令重新启动程序,然后用break 命令设置断点。上述输出结果证实,名称为1 的断点被设置在main.go的第9行。main.go中的第9行。
现在让我们把我们的程序continue ,检查调试器是否在断点处暂停程序。
(dlv) continue
> main.main() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x10c16e4)
4: "fmt"
5: )
6:
7: func main() {
8: arr := []int{101, 95, 10, 188, 100}
=> 9: max := arr[0]
10: for _, v := range arr {
11: if v > max {
12: max = v
13: }
14: }
在执行continue ,我们可以看到调试器在第9行暂停了我们的程序。这正是我们想要的:)。
列出断点
(dlv) breakpoints
上面的命令列出了当前程序的断点。
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x102de10 for runtime.fatalthrow() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:820 (0)
Breakpoint unrecovered-panic at 0x102de80 for runtime.fatalpanic() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:847 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x10c16e4 for main.main() ./main.go:9 (1)
你可能会惊讶地发现,除了我们添加的那个断点外,还有另外两个断点。另外两个断点是由delve添加的,以确保调试会话不会在出现未用*recover处理的运行时恐慌*时突然结束。
打印变量
该程序的执行在第9行暂停了。9。print 是用来打印变量值的命令。让我们使用print ,打印分片的第0个索引的元素arr 。
(dlv) print arr[0]
运行上述命令将打印101 ,它是分片arr 的第0个索引的元素。
请注意,如果我们试图打印max ,我们会得到一个垃圾值。
(dlv) print max
824634294736
这是因为程序在执行第9行之前已经暂停了。这是因为程序在执行第9行之前已经暂停,因此打印max ,会打印出一些随机的垃圾值。为了打印max的实际值,我们应该移到程序的下一行。这可以用next 命令来完成。
移动到源代码中的下一行
(dlv) next
将会把调试器移到下一行,并且会输出。
> main.main() ./main.go:10 (PC: 0x10c16ee)
5: )
6:
7: func main() {
8: arr := []int{101, 95, 10, 188, 100}
9: max := arr[0]
=> 10: for _, v := range arr {
11: if v > max {
12: max = v
13: }
14: }
15: fmt.Printf("Max element is %d\n", max)
现在如果我们尝试(dlv) print max ,我们可以看到输出101 。
next命令可以用来逐行浏览一个程序。
如果你继续输入next ,你可以看到调试器在程序中逐行走过。当第10行的for 循环的一次迭代结束后, 。10行的循环结束后,next 将带领我们进行下一次迭代,程序将最终终止。
打印表达式
print也可以用来评估表达式。例如,如果我们想找到max + 10 的值,就可以用print。
让我们在for 循环外添加另一个断点,在这里将完成max 的计算。
(dlv) break main.go:15
上面的命令在第15行添加了一个断点。15行添加了另一个断点,在这里max的计算已经完成。
输入continue ,程序将在此断点处停止。
print max+10命令将输出198。
清除断点
clear是清除单个断点的命令,clearall是清除程序中所有断点的命令。
让我们首先列出我们应用程序中的断点。
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x102de10 for runtime.fatalthrow() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:820 (0)
Breakpoint unrecovered-panic at 0x102de80 for runtime.fatalpanic() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:847 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x10c16e4 for main.main() ./main.go:9 (1)
Breakpoint 2 at 0x10c1785 for main.main() ./main.go:15 (1)
我们有两个断点,分别名为1 和2
如果我们运行clear 1 ,它将删除断点1 。
(dlv) clear 1
Breakpoint 1 cleared at 0x10c16e4 for main.main() ./main.go:9
如果我们运行clearall ,它将删除所有断点。我们只剩下一个名为2 的断点。
(dlv) clearall
Breakpoint 2 cleared at 0x10c1785 for main.main() ./main.go:15
从上面的输出中,我们可以看到,剩下的一个断点也被清除了。如果我们现在执行continue 命令,程序将打印出max 的值并终止。
(dlv) continue
Max element is 188
Process 3095 has exited with status 0
踏入和踏出一个函数
我们可以用Delve来进入一个函数或退出一个函数。不要担心,如果现在没有意义的话:)。让我们试着借助一个例子来理解这个问题。
package main
import (
"fmt"
)
func max(arr []int) int {
max := arr[0]
for _, v := range arr {
if v > max {
max = v
}
}
return max
}
func main() {
arr := []int{101, 95, 10, 188, 100}
m := max(arr)
fmt.Printf("Max element is %d\n", m)
}
我修改了到目前为止我们一直在使用的程序,并将找到切片中最大元素的逻辑移到自己的函数中,命名为max 。
使用(dlv) q 退出Delve,用上面的程序替换main.go ,然后使用dlv debug 命令再次开始调试。
让我们在第18行添加一个断点,即调用max 函数的地方。
b是添加断点的简写。让我们使用这个。
(dlv) b main.go:18
(dlv) continue
我们已经在第18行添加了断点,并继续执行程序。运行上述命令将打印。
> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)
13: }
14: return max
15: }
16: func main() {
17: arr := []int{101, 95, 10, 188, 100}
=> 18: m := max(arr)
19: fmt.Printf("Max element is %d\n", m)
20: }
程序的执行在第18行暂停了。18行暂停,正如预期的那样。现在我们有两个选择。
- 继续深入调试
max函数 - 跳过max函数,转到下一行。
根据我们的要求,我们可以做任何一种。让我们来学习一下如何做这两件事。
首先,让我们跳过max函数,转到下一行。要做到这一点,你可以直接运行next ,调试器会自动移动到下一行。默认情况下,Delve不会深入到函数调用。
(dlv) next
> main.main() ./main.go:19 (PC: 0x10c17d3)
14: return max
15: }
16: func main() {
17: arr := []int{101, 95, 10, 188, 100}
18: m := max(arr)
=> 19: fmt.Printf("Max element is %d\n", m)
20: }
你可以从上面的输出中看到,调试器已经移到了下一行。
输入continue ,程序将完成执行。
让我们来学习如何深入到max函数中去。
输入restart 和continue ,我们可以看到程序在已经存在的断点处再次暂停。
(dlv) restart
Process restarted with PID 5378
(dlv) continue
> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)
13: }
14: return max
15: }
16: func main() {
17: arr := []int{101, 95, 10, 188, 100}
=> 18: m := max(arr)
19: fmt.Printf("Max element is %d\n", m)
20: }
现在输入step ,我们可以看到控制现在已经移到了max 函数中。
(dlv) step
> main.max() ./main.go:7 (PC: 0x10c1650)
2:
3: import (
4: "fmt"
5: )
6:
=> 7: func max(arr []int) int {
8: max := arr[0]
9: for _, v := range arr {
10: if v > max {
11: max = v
12: }
输入next ,控件将移动到max 函数的第一行。
(dlv) next
> main.max() ./main.go:8 (PC: 0x10c1667)
3: import (
4: "fmt"
5: )
6:
7: func max(arr []int) int {
=> 8: max := arr[0]
9: for _, v := range arr {
10: if v > max {
11: max = v
12: }
13: }
如果你继续输入next ,你就可以踏过max 函数的执行路径。
你可能想知道是否有可能不通过max 函数中的每一行就返回到main 。是的,使用stepout 命令可以做到这一点。
(dlv) stepout
> main.main() ./main.go:18 (PC: 0x10c17c9)
Values returned:
~r1: 188
13: }
14: return max
15: }
16: func main() {
17: arr := []int{101, 95, 10, 188, 100}
=> 18: m := max(arr)
19: fmt.Printf("Max element is %d\n", m)
20: }
一旦你输入stepout ,控件就会返回到main。现在你可以继续在main 中进行调试 :)。
打印堆栈跟踪
调试时需要的一个非常重要的功能是打印程序的当前堆栈跟踪。这对找出当前代码的执行路径很有用。stack 是用来打印当前堆栈跟踪的命令。
让我们清除所有的断点,在第11行添加一个新的断点。11行添加一个新的断点,并打印当前程序的堆栈跟踪。
(dlv) restart
(dlv) clearall
(dlv) b main.go:11
(dlv) continue
当程序在断点处暂停时,输入
(dlv) stack
它将输出程序的当前堆栈跟踪。
0 0x00000000010c16e8 in main.max
at ./main.go:11
1 0x00000000010c17c9 in main.main
at ./main.go:18
2 0x000000000102f754 in runtime.main
at /usr/local/Cellar/go/1.13.7/libexec/src/runtime/proc.go:203
3 0x000000000105acc1 in runtime.goexit
at /usr/local/Cellar/go/1.13.7/libexec/src/runtime/asm_amd64.s:1357
到目前为止,我们已经涵盖了基本命令,以帮助开始使用Delve调试你的应用程序。在接下来的教程中,我们将介绍Delve的高级功能,如调试goroutines,将调试器附加到现有进程,远程调试,以及从VSCode编辑器中使用Delve。
谢谢你的阅读。请留下您的评论和反馈。