需要调试器
在任何编程语言中,最简单的调试方式是使用打印语句/日志,并写成标准输出。这绝对是可行的,但当我们的应用程序的规模增长和逻辑变得更加复杂时,这就变得非常困难。将打印语句添加到应用程序的每个代码路径中并不容易。这就是调试器的用武之地。调试器帮助我们使用断点和一系列其他功能来跟踪程序的执行路径。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。
让我们通过运行 export PATH=$PATH:~/go/bin 将 GOBIN 添加到 PATH 中。
你操作系统是 macOS 的话,运行 Delve 需要使用 Xcode 命令行开发工具,请运行 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 开始调试它。
让我们使用下面的命令为我们的示例程序创建一个目录。
在我们刚才创建的 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
现在我们已经准备好调试程序了。输入命令 cd ~/Documents/debugsample 让我们移动到 debugsample 目录下。之后,输入以下命令来启动 Delve。
dlv debug
上述命令将开始调试当前目录下的 main package 。输入上面的命令后,你可以看到终端已经变成了 (dlv) 提示。如果你能看到这个变化,说明调试器已经成功启动,正在等待我们的命令:)。)
让我们启动我们的第一个命令。
在 dlv 提示符下,输入 continue。
(dlv) continue
continue
命令将运行程序,直到出现断点或程序完成。由于我们没有定义任何断点,
所以程序将一直运行到完成。
**
Max element is 188
Process 1733 has exited with status 0
如果你看到上面的输出,说明调试器已经运行,程序已经完成:)。) 但这对我们没有任何用处。让我们继续添加几个断点,看着调试器施展它的魔法。
创建断点
断点可以将程序的执行暂停在指定的行。当执行暂停时,我们可以向调试器发送命令,打印变量的值,查看程序的堆栈跟踪等。
下面提供了创建断点的语法。
(dlv) break filename:lineno
上面的命令将在文件名的第 lineno 行创建一个断点。
让我们在 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 设置断点。上面的输出确认在 main.go 的第 9 行设置了名称为 1 的断点。
现在我们继续我们的程序,检查调试器是否在断点处暂停程序。
(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 添加的,目的是为了确保当出现运行时的 panic 而没有使用 recover 处理时,调试会话不会突然结束。
打印变量
程序的执行在第 9 行暂停了。print 是用来打印变量值的命令。让我们使用 print 打印切片 arr 的索引为 0 的元素。
(dlv) print arr[0]
运行上面的命令将打印 101,也就是切片 arr 的第 0 个索引的元素。
请注意,如果我们尝试打印 max,我们将得到一个废弃值。
(dlv) print max
824634294736
这是因为程序在第 9 行执行之前暂停了,因此打印 max 会打印一些随机的废弃值。第 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 循环一个迭代结束时,next 将引导我们完成下一个迭代,程序最终将终止。
打印表达式
_print 也可以用来对表达式求值。例如,如果我们想找到 max + 10 的值,就可以使用print。
让我们在 for 循环外再添加一个断点,在这里完成 max 的计算。
(dlv) break main.go:15
上面的命令在第 15 行增加了一个断点,在这里完成了对 max 的计算。在第 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 命令再次开始调试。
让我们在调用 max 函数的第18行添加一个断点。
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 行已经如期暂停。现在我们有两个选择。
- 继续深入调试 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,控件将移动到最大函数的第一行。
(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 行添加一个新的断点,并打印当前的堆栈跟踪。
(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。
感谢您的阅读,请留下您的意见和反馈。请留下您的意见和反馈。
喜欢我的教程?请支持我的内容。