需要调试器

在任何编程语言中,最简单的调试方式是使用打印语句/日志,并写成标准输出。这绝对是可行的,但当我们的应用程序的规模增长和逻辑变得更加复杂时,这就变得非常困难。将打印语句添加到应用程序的每个代码路径中并不容易。这就是调试器的用武之地。调试器帮助我们使用断点和一系列其他功能来跟踪程序的执行路径。Delve 就是这样一个针对 Go 的调试器。在本教程中,我们将学习如何使用Delve调试Go应用程序。

安装 Delve

请确保你在一个不包含 go.mod 文件的目录中。我自己就喜欢在 Documents 目录。

  1. cd ~/Documents/

接下来,让我们设置 GOBIN 环境变量。这个环境变量指定了 Delve 二进制文件的安装位置,如果你已经设置了 GOBIN,请跳过这一步。你可以通过运行下面的命令来检查 GOBIN 是否被设置。

  1. 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。请运行

  1. go get github.com/go-delve/delve/cmd/dlv

运行此命令后,安装 delve。请运行 dlv version 来测试你的安装。它将在安装成功后打印出 Delve 的版本。

  1. Delve Debugger
  2. Version: 1.4.0
  3. Build: $Id: 67422e6f7148fa1efa0eac1423ab5594b223d93b

开始 Delve

让我们写一个简单的程序,然后用 Delve 开始调试它。

让我们使用下面的命令为我们的示例程序创建一个目录。

在我们刚才创建的 debugsample 目录下创建一个文件 main.go,内容如下。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. arr := []int{101, 95, 10, 188, 100}
  7. max := arr[0]
  8. for _, v := range arr {
  9. if v > max {
  10. max = v
  11. }
  12. }
  13. fmt.Printf("Max element is %d\n", max)
  14. }

上面的程序会打印出切片 arr 的最大元素。运行上面的程序将输出。

  1. Max element is 188

现在我们已经准备好调试程序了。输入命令 cd ~/Documents/debugsample 让我们移动到 debugsample 目录下。之后,输入以下命令来启动 Delve。

  1. dlv debug

上述命令将开始调试当前目录下的 main package 。输入上面的命令后,你可以看到终端已经变成了 (dlv) 提示。如果你能看到这个变化,说明调试器已经成功启动,正在等待我们的命令:)。)

让我们启动我们的第一个命令。

在 dlv 提示符下,输入 continue。

  1. (dlv) continue

continue 命令将运行程序,直到出现断点或程序完成。由于我们没有定义任何断点,
所以程序将一直运行到完成。
**

  1. Max element is 188
  2. Process 1733 has exited with status 0

如果你看到上面的输出,说明调试器已经运行,程序已经完成:)。) 但这对我们没有任何用处。让我们继续添加几个断点,看着调试器施展它的魔法。

创建断点

断点可以将程序的执行暂停在指定的行。当执行暂停时,我们可以向调试器发送命令,打印变量的值,查看程序的堆栈跟踪等。

下面提供了创建断点的语法。

  1. (dlv) break filename:lineno

上面的命令将在文件名的第 lineno 行创建一个断点。

让我们在 main.go 的第 9 行添加一个断点。

  1. dlv) break main.go:9

当运行上面的命令时,可以看到输出,Process 1733 has exited with status 0,实际上并没有添加断点。这是因为我们之前运行 continue 的时候,因为当时没有断点,所以程序已经退出了。我们重启程序,再尝试设置断点。

  1. (dlv) restart
  2. Process restarted with PID 2028
  3. (dlv) break main.go:9
  4. Breakpoint 1 set at 0x10c16e4 for main.main() ./main.go:9

restart 命令重新启动程序,然后命令 break 设置断点。上面的输出确认在 main.go 的第 9 行设置了名称为 1 的断点。

现在我们继续我们的程序,检查调试器是否在断点处暂停程序。

  1. (dlv) continue
  1. > main.main() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x10c16e4)
  2. 4: "fmt"
  3. 5: )
  4. 6:
  5. 7: func main() {
  6. 8: arr := []int{101, 95, 10, 188, 100}
  7. => 9: max := arr[0]
  8. 10: for _, v := range arr {
  9. 11: if v > max {
  10. 12: max = v
  11. 13: }
  12. 14: }

continue 命令执行后,我们可以看到调试器在第 9 行暂停了我们的程序。这正是我们想要的:)。

列出断点

  1. (dlv) breakpoints

上面的命令列出了应用程序的当前断点。

  1. (dlv) breakpoints
  2. Breakpoint runtime-fatal-throw at 0x102de10 for runtime.fatalthrow() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:820 (0)
  3. Breakpoint unrecovered-panic at 0x102de80 for runtime.fatalpanic() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:847 (0)
  4. print runtime.curg._panic.arg
  5. Breakpoint 1 at 0x10c16e4 for main.main() ./main.go:9 (1)

你可能会惊讶地发现,除了我们添加的断点之外,还有另外两个断点。另外两个断点是由 delve 添加的,目的是为了确保当出现运行时的 panic 而没有使用 recover 处理时,调试会话不会突然结束。

打印变量

程序的执行在第 9 行暂停了。print 是用来打印变量值的命令。让我们使用 print 打印切片 arr 的索引为 0 的元素。

  1. (dlv) print arr[0]

运行上面的命令将打印 101,也就是切片 arr 的第 0 个索引的元素。

请注意,如果我们尝试打印 max,我们将得到一个废弃值。

  1. (dlv) print max
  2. 824634294736

这是因为程序在第 9 行执行之前暂停了,因此打印 max 会打印一些随机的废弃值。第 9 行执行之前,程序已经暂停,因此打印 max 会打印一些随机的垃圾值。要打印 max 的实际值,我们应该移动到程序的下一行。这可以通过 next 命令来完成。

移动到源文件的下一行

  1. (dlv) next

将调试器移至下一行,并输出,

  1. > main.main() ./main.go:10 (PC: 0x10c16ee)
  2. 5: )
  3. 6:
  4. 7: func main() {
  5. 8: arr := []int{101, 95, 10, 188, 100}
  6. 9: max := arr[0]
  7. => 10: for _, v := range arr {
  8. 11: if v > max {
  9. 12: max = v
  10. 13: }
  11. 14: }
  12. 15: fmt.Printf("Max element is %d\n", max)

移动到源代码中的下一行会将调试器移动到下一行,它将输出。现在,如果我们尝试 (dlv) print max,我们可以看到输出 101。

next 命令可用于逐行浏览程序。

如果继续输入 next,则可以看到调试器在程序中逐行进行调试。 当第 10 行 for 循环一个迭代结束时,next 将引导我们完成下一个迭代,程序最终将终止。

打印表达式


_print
也可以用来对表达式求值。例如,如果我们想找到 max + 10 的值,就可以使用print。

让我们在 for 循环外再添加一个断点,在这里完成 max 的计算。

  1. (dlv) break main.go:15

上面的命令在第 15 行增加了一个断点,在这里完成了对 max 的计算。在第 15 行增加一个断点,在这里完成了对max 的计算。

输入 continue,程序将在这个断点处停止。

print max+10 命令将输出 198

清除断点

clear 是清除单个断点的命令,clearall 是清除程序中所有断点的命令。

首先让我们列出程序中的断点。

  1. (dlv) breakpoints
  2. Breakpoint runtime-fatal-throw at 0x102de10 for runtime.fatalthrow() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:820 (0)
  3. Breakpoint unrecovered-panic at 0x102de80 for runtime.fatalpanic() /usr/local/Cellar/go/1.13.7/libexec/src/runtime/panic.go:847 (0)
  4. print runtime.curg._panic.arg
  5. Breakpoint 1 at 0x10c16e4 for main.main() ./main.go:9 (1)
  6. Breakpoint 2 at 0x10c1785 for main.main() ./main.go:15 (1)

我们有两个断点,分别是 1 和 2。

如果我们运行 clear 1,就会删除断点 1。

  1. (dlv) clear 1
  2. Breakpoint 1 cleared at 0x10c16e4 for main.main() ./main.go:9

如果我们运行 clearall,它将删除所有断点。我们只剩下一个名为 2 的断点。

  1. (dlv) clearall
  2. Breakpoint 2 cleared at 0x10c1785 for main.main() ./main.go:15

从上面的输出中,我们可以看到剩下的一个断点也被清除了。如果我们现在执行 continue 命令,程序将打印出 max 值并终止。

  1. (dlv) continue
  2. Max element is 188
  3. Process 3095 has exited with status 0

进入和退出函数

可以使用 Delve 进入函数或退出函数。如果现在还不明白,也不要担心:)。让我们通过一个例子来理解这个问题。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func max(arr []int) int {
  6. max := arr[0]
  7. for _, v := range arr {
  8. if v > max {
  9. max = v
  10. }
  11. }
  12. return max
  13. }
  14. func main() {
  15. arr := []int{101, 95, 10, 188, 100}
  16. m := max(arr)
  17. fmt.Printf("Max element is %d\n", m)
  18. }

我修改了我们一直在使用的程序,并将寻找片中最大元素的逻辑移到了max 函数中。

使用 (dlv) q 退出 Delve,用上面的程序替换 main.go,然后使用 dlv debug 命令再次开始调试。

让我们在调用 max 函数的第18行添加一个断点。

b 是添加断点的简写。让我们使用这个命令。

  1. (dlv) b main.go:18
  2. (dlv) continue

我们在第 18 行添加了断点,继续执行程序。运行上面的命令会打印出

  1. > main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)
  2. 13: }
  3. 14: return max
  4. 15: }
  5. 16: func main() {
  6. 17: arr := []int{101, 95, 10, 188, 100}
  7. => 18: m := max(arr)
  8. 19: fmt.Printf("Max element is %d\n", m)
  9. 20: }

程序的执行在 18 行已经如期暂停。现在我们有两个选择。

  • 继续深入调试 max 函数。


  • 跳过最大函数,转到下一行。

根据我们的要求,我们可以做其中之一。让我们来学习如何做到这两点。

首先,让我们跳过 max 函数并移动到下一行。要做到这一点,你可以直接运行 next,调试器会自动移动到下一行。默认情况下,Delve 不会深入到函数调用中去。

  1. (dlv) next
  2. > main.main() ./main.go:19 (PC: 0x10c17d3)
  3. 14: return max
  4. 15: }
  5. 16: func main() {
  6. 17: arr := []int{101, 95, 10, 188, 100}
  7. 18: m := max(arr)
  8. => 19: fmt.Printf("Max element is %d\n", m)
  9. 20: }

从上面的输出可以看出,调试器已经移动到下一行。

输入 continue,程序将完成执行。

让我们来学习如何更深入地研究 max 函数。

输入 restart 和 continue,我们可以看到程序再次暂停在已经存在的断点处。

  1. (dlv) restart
  2. Process restarted with PID 5378
  3. (dlv) continue
  4. > main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)
  5. 13: }
  6. 14: return max
  7. 15: }
  8. 16: func main() {
  9. 17: arr := []int{101, 95, 10, 188, 100}
  10. => 18: m := max(arr)
  11. 19: fmt.Printf("Max element is %d\n", m)
  12. 20: }

现在输入 step,我们可以看到,现在控件已经进入了 max 函数。

  1. (dlv) step
  2. > main.max() ./main.go:7 (PC: 0x10c1650)
  3. 2:
  4. 3: import (
  5. 4: "fmt"
  6. 5: )
  7. 6:
  8. => 7: func max(arr []int) int {
  9. 8: max := arr[0]
  10. 9: for _, v := range arr {
  11. 10: if v > max {
  12. 11: max = v
  13. 12: }

输入 next,控件将移动到最大函数的第一行。

  1. (dlv) next
  2. > main.max() ./main.go:8 (PC: 0x10c1667)
  3. 3: import (
  4. 4: "fmt"
  5. 5: )
  6. 6:
  7. 7: func max(arr []int) int {
  8. => 8: max := arr[0]
  9. 9: for _, v := range arr {
  10. 10: if v > max {
  11. 11: max = v
  12. 12: }
  13. 13: }

如果你继续键入 next,你就可以跳过 max 函数的执行路径。

你可能想知道,是否可以不经过 max 函数的每一行就返回 main。是的,使用 stepout 命令是可以的。

  1. (dlv) stepout
  2. > main.main() ./main.go:18 (PC: 0x10c17c9)
  3. Values returned:
  4. ~r1: 188
  5. 13: }
  6. 14: return max
  7. 15: }
  8. 16: func main() {
  9. 17: arr := []int{101, 95, 10, 188, 100}
  10. => 18: m := max(arr)
  11. 19: fmt.Printf("Max element is %d\n", m)
  12. 20: }

一旦你输入了 stepout,控件就会返回到 main。现在你可以在 main 中继续调试了:)

打印堆栈跟踪

调试时需要一个非常重要的功能,就是打印程序当前的堆栈跟踪。这对于找出当前代码的执行路径非常有用,stack 是用来打印当前堆栈跟踪的命令。

让我们清除所有的断点,在第 11 行添加一个新的断点,并打印当前的堆栈跟踪。

  1. (dlv) restart
  2. (dlv) clearall
  3. (dlv) b main.go:11
  4. (dlv) continue

当程序在断点处暂停时,输入

  1. (dlv) stack

它将输出当前程序的堆栈跟踪。

  1. 0 0x00000000010c16e8 in main.max
  2. at ./main.go:11
  3. 1 0x00000000010c17c9 in main.main
  4. at ./main.go:18
  5. 2 0x000000000102f754 in runtime.main
  6. at /usr/local/Cellar/go/1.13.7/libexec/src/runtime/proc.go:203
  7. 3 0x000000000105acc1 in runtime.goexit
  8. at /usr/local/Cellar/go/1.13.7/libexec/src/runtime/asm_amd64.s:1357

到目前为止,我们已经介绍了使用 Delve 调试应用程序的基本命令。在即将到来的教程中,我们将介绍 Delve 的高级功能,如调试 goroutines、将调试器附加到现有进程、远程调试以及从 VSCode 编辑器中使用 Delve。

感谢您的阅读,请留下您的意见和反馈。请留下您的意见和反馈。

喜欢我的教程?请支持我的内容

原文链接

https://golangbot.com/debugging-go-delve/