需要调试器
在任何编程语言中,最简单的调试方式是使用打印语句/日志,并写成标准输出。这绝对是可行的,但当我们的应用程序的规模增长和逻辑变得更加复杂时,这就变得非常困难。将打印语句添加到应用程序的每个代码路径中并不容易。这就是调试器的用武之地。调试器帮助我们使用断点和一系列其他功能来跟踪程序的执行路径。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 DebuggerVersion: 1.4.0Build: $Id: 67422e6f7148fa1efa0eac1423ab5594b223d93b
开始 Delve
让我们写一个简单的程序,然后用 Delve 开始调试它。
让我们使用下面的命令为我们的示例程序创建一个目录。
在我们刚才创建的 debugsample 目录下创建一个文件 main.go,内容如下。
package mainimport ("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 188Process 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) restartProcess restarted with PID 2028(dlv) break main.go:9Breakpoint 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 = v13: }14: }
continue 命令执行后,我们可以看到调试器在第 9 行暂停了我们的程序。这正是我们想要的:)。
列出断点
(dlv) breakpoints
上面的命令列出了应用程序的当前断点。
(dlv) breakpointsBreakpoint 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.argBreakpoint 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 max824634294736
这是因为程序在第 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 = v13: }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) breakpointsBreakpoint 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.argBreakpoint 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 1Breakpoint 1 cleared at 0x10c16e4 for main.main() ./main.go:9
如果我们运行 clearall,它将删除所有断点。我们只剩下一个名为 2 的断点。
(dlv) clearallBreakpoint 2 cleared at 0x10c1785 for main.main() ./main.go:15
从上面的输出中,我们可以看到剩下的一个断点也被清除了。如果我们现在执行 continue 命令,程序将打印出 max 值并终止。
(dlv) continueMax element is 188Process 3095 has exited with status 0
进入和退出函数
可以使用 Delve 进入函数或退出函数。如果现在还不明白,也不要担心:)。让我们通过一个例子来理解这个问题。
package mainimport ("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 max15: }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 max15: }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) restartProcess restarted with PID 5378(dlv) continue> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)13: }14: return max15: }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 = v12: }
输入 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 = v12: }13: }
如果你继续键入 next,你就可以跳过 max 函数的执行路径。
你可能想知道,是否可以不经过 max 函数的每一行就返回 main。是的,使用 stepout 命令是可以的。
(dlv) stepout> main.main() ./main.go:18 (PC: 0x10c17c9)Values returned:~r1: 18813: }14: return max15: }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.maxat ./main.go:111 0x00000000010c17c9 in main.mainat ./main.go:182 0x000000000102f754 in runtime.mainat /usr/local/Cellar/go/1.13.7/libexec/src/runtime/proc.go:2033 0x000000000105acc1 in runtime.goexitat /usr/local/Cellar/go/1.13.7/libexec/src/runtime/asm_amd64.s:1357
到目前为止,我们已经介绍了使用 Delve 调试应用程序的基本命令。在即将到来的教程中,我们将介绍 Delve 的高级功能,如调试 goroutines、将调试器附加到现有进程、远程调试以及从 VSCode 编辑器中使用 Delve。
感谢您的阅读,请留下您的意见和反馈。请留下您的意见和反馈。
喜欢我的教程?请支持我的内容。
