本文最初发表于Golang Cafe-您可以在这里阅读原文 https://golang.cafe/blog/golang-debugging-with-delve.html

在本文中,我们将了解如何使用 Delve 调试 Go (Golang) 程序。 Delve 是 Go 编程语言的第三方调试器,可在 githubhttps://github.com/go-delve/delve上找到。它是 GDB golang 调试器 (https://golang.org/doc/gdb) 的有效替代品,因为它的功能更丰富,如官方Go GDB 网站中所述

请注意,在调试使用标准工具链构建的 Go 程序时,Delve是 GDB 的更好替代方案。它比 GDB 更了解 Go 运行时、数据结构和表达式。 Delve 目前在 amd64 上支持 Linux、OSX 和 Windows。有关受支持平台的最新列表,请参阅Delve 文档。

目标

在本文结束时,您将能够使用 delve 调试器命令行工具轻松调试和检查 Go 程序。我们将看到如何在 Go 程序中查看、添加和更改断点,逐行或通过断点导航程序,检查变量、函数和表达式值,最后详细分析我们所有的程序。

下载并安装 Go Delve

只需使用 go get 命令即可下载和安装 Go delve

Linux、Windows、OSX

$ 去获取 github.com/go-delve/delve/cmd/dlv

  • 如果您使用 Go 模块,您可能希望在项目目录之外执行此命令,以避免将 delve 作为依赖项添加到 go.mod 文件中

如果你有一个有效的 Go 安装,则应该已经设置了以下内容:

  • 确保 GOBIN 环境变量设置正确,这将指示 dlv (delve) 命令将存储的目录。您可以通过键入 go env GOBIN 进行检查

  • 确保 PATH 包含 GOBIN,这将允许您在不指定绝对路径的情况下运行 Go 二进制可执行文件

OSX

在 Mac OS 中,您可能还需要通过键入以下内容来启用开发人员工具

xcode-选择--安装

检查您的安装

完成安装后,您可以通过检查版本来检查 delve 是否已成功安装

$dlv 版本

Delve 调试器

版本:1.5.1

国家:$,如:

这意味着您已经成功安装了 delve。现在让我们开始调试。

开始调试(Delve服务器)

当我们开始调试时,我们通常的意思是,我们开始一个“调试会话”。要启动调试会话,您可以使用 dlv 命令行帮助中提供的命令之一

$ dlv 帮助

...

用法:

dlv [命令]

可用命令:

attach 附加到正在运行的进程并开始调试。

connect 连接到无头调试服务器。

core 检查核心转储。

dap [EXPERIMENTAL] 启动通过调试适配器协议 (DAP) 进行通信的 TCP 服务器。

debug 编译并开始调试当前目录或指定包中的主包。

exec 执行预编译的二进制文件,并开始调试会话。

help 关于任何命令的帮助

运行已弃用的命令。请改用“调试”。

test 编译测试二进制文件并开始调试程序。

trace 编译并开始跟踪程序。

version 打印版本。

我们感兴趣的命令是 dlv debug 和 dlv exec,它们都用于启动调试会话,唯一的区别是一个(dlv debug)也从源代码编译二进制文件,另一个(dlv exec)期望有一个编译的二进制文件。

如果我们想调试 Go 测试,test 命令也很有用。

Golang调试代码示例

下面的代码片段代表我们将用于调试会话的代码示例,它是一个斐波那契实现。

包主

导入“fmt”

var m u003d make(map[int]int, 0)

功能主要(){

对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

x :u003d fib(n)

fmt.Println(n, "fib", x)

}

}

func fib(n int) int {

如果 n < 2 {

返回 n

}

var f int
if v, ok := m[n]; ok {
    f = v
} else {
    f = fib(n-2) + fib(n-1)
    m[n] = f
}

return f

进入全屏模式 退出全屏模式

}

您可以在Go Playground中检查并运行此代码片段

我们需要传递一个主包,它将被编译和执行以进行调试。

dlv 调试 main.go

键入“帮助”以获取命令列表。

(dlv)

这将启动调试会话。这也被称为 delve 服务器,因为它是一个等待指令的正在运行的进程。

Delve 客户端

一旦我们的调试会话开始,我们已经编译并附加到一个 Go 二进制文件,我们可以开始调试我们的程序。我们现在看到了一个新的 repl,它只是一个 delve 解释器,您也可以将其称为“delve 客户端”,它将调试指令发送到我们之前创建的 delve 服务器。

我们可以通过键入以下内容来查看所有可用的命令。

键入“帮助”以获取命令列表。

(dlv) 帮助

以下命令可用:

运行程序:

call ------------------------ 恢复进程,注入函数调用(实验!!!)

继续(别名:c) --------- 运行直到断点或程序终止。

下一个(别名:n) ------------- 跳到下一个源代码行。

重建 --------- 重建目标可执行文件并重新启动它。如果可执行文件不是由 delve 构建的,则它不起作用。

restart (别名: r) ---------- 重启进程。

step (别名: s) ------------- 单步执行程序。

step-instruction(别名:si)单步单步cpu指令。

stepout (别名: so) --------- 跳出当前函数。

操作断点:

break (alias: b) ------- 设置断点。

断点(别名:bp)打印活动断点的信息。

clear ------------------ 删除断点。

clearall --------------- 删除多个断点。

条件(别名:cond)设置断点条件。

on --------------------- 遇到断点时执行命令。

跟踪(别名:t)------- 设置跟踪点。

查看程序变量和内存:

args ----------------- 打印函数参数。

display -------------- 每次程序停止时打印表达式的值。

检查内存(别名:x)检查内存:

locals --------------- 打印局部变量。

print (alias: p) ----- 计算一个表达式。

regs ----------------- 打印 CPU 寄存器的内容。

set ------------------ 更改变量的值。

vars ----------------- 打印包变量。

什么是 --------------- 表达式的打印类型。

在线程和 goroutine 之间列出和切换:

goroutine (alias: gr) -- 显示或改变当前的 goroutine

goroutines(别名:grs)列出程序 goroutines。

thread(别名:tr)-----切换到指定线程。

线程 ---------------- 打印出每个跟踪线程的信息。

查看调用堆栈并选择帧:

deferred --------- 在延迟调用的上下文中执行命令。

down ------------- 向下移动当前帧。

frame ------------ 设置当前帧,或在不同的帧上执行命令。

堆栈(别名:bt)打印堆栈跟踪。

up --------------- 向上移动当前帧。

其他命令:

config --------------------- 更改配置参数。

disassemble(别名:disass) 反汇编程序。

编辑(别名:ed) ----------- 打开您在 $DELVE_EDITOR 或 $EDITOR 中的位置

exit (别名:quit | q) ----- 退出调试器。

funcs ---------- 打印函数列表。

help (别名: h) ------------ 打印帮助信息。

库 ------------------ 列出加载的动态库

list (别名: ls | l) ------- 显示源代码。

source --------------------- 执行包含 delve 命令列表的文件

sources -------------------- 打印源文件列表。

types ---------------------- 打印类型列表

键入 help 后跟一个命令以获取完整文档。

有一些可用的命令,但我们可以轻松地将它们拆分为不同的重点区域,以更好地了解如何接近 delve 客户端。

通用 Delve 命令

我要提请您注意的第一个命令是 list 命令,它允许我们列出给定位置的源代码。我们可以通过传递包名和函数或文件路径和行来指定位置。例如

列表

按包和函数名显示源代码

(dlv) 列出 main.main

显示 /workspace/tutorials/delve/main.go:7 (PC: 0x10d145b)

2:

3:导入“fmt”

4:

5: var m u003d make(map[int]int, 0)

6:

7:函数主(){

8: 对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

9: x :u003d fib(n)

10: fmt.Println(n, "fib", x)

11:}

12:}

(dlv)

列表

按文件名和行号显示源代码

(dlv) 列表 ./main.go:14

显示 /workspace/tutorials/delve/main.go:14 (PC: 0x10d1713)

9: x :u003d fib(n)

10: fmt.Println(n, "fib", x)

11:}

12:}

13:

14: func fib(n int) int {

15: 如果 n < 2 {

16:返回 n

17:}

18:

19: 变量 f 整数

(dlv)

还有一个命令可以搜索给定模式的函数

功能

(dlv) funcs fib

主文件

出口

如果您卡在调试会话中,您可以退出

(dlv) 退出

用 Delve 添加断点

一旦您知道如何使用 delve list 命令在屏幕上显示源代码的一部分,您可能希望开始在程序中添加断点,然后在您想要停止并检查变量和其他表达式的区域中价值观。

对于这个基本示例,假设我们想在 main.go 文件的第 10 行添加一个断点,我们已经在前面的 list 示例中看到过。这将通过使用 break 关键字后跟要添加断点的位置来完成。

休息

这将在指定位置添加一个断点,并在第 10 行列出该断点的使用位置

(dlv) 中断 ./main.go:10

断点 1 设置为 main.main() 的 0x10d155d ./main.go:10

(dlv) 列表 ./main.go:10

显示 /workspace/tutorials/delve/main.go:10 (PC: 0x10d155d)

5: var m u003d make(map[int]int, 0)

6:

7:函数主(){

8: 对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

9: x :u003d fib(n)

10: fmt.Println(n, "fib", x)

11:}

12:}

13:

14: func fib(n int) int {

15: 如果 n < 2 {

(dlv)

断点

列出此调试会话的所有当前断点

(dlv) 断点

断点 runtime-fatal-throw at 0x10388c0 for runtime.fatalthrow() /usr/local/go/src/runtime/panic.go:1162 (0)

runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1189 (0) 的断点未恢复-恐慌在 0x1038940

打印 runtime.curg._panic.arg

main.main() ./main.go:10 (0) 的 0x10d155d 处的断点 1

在这个例子中,我们可以看到 3 个断点。前 2 个由 delve 自动添加并用作恐慌和致命错误的保障,以便我们能够查明程序的状态并检查变量、堆栈跟踪和状态。

第三个断点表示断点 1 是我们在第 10 行添加的断点。

尝试添加新的断点,然后在此处的列表中查看如何显示!

清除

从调试会话中删除特定断点

(dlv) 清除 1

断点 1 在 0x10d155d 清除 main.main() ./main.go:10

如果您想删除错误添加的特定断点,或者只是因为您需要删除并开始调试同一程序的其他区域,这将非常有用。

清除

清理所有手动添加的断点

(dlv) 中断 ./main.go:8

断点 1 为 main.main() 设置在 0x10d1472 ./main.go:8

(dlv) 中断 ./main.go:9

断点 2 为 main.main() 设置在 0x10d154a ./main.go:9

(dlv) 中断 ./main.go:10

断点 3 为 main.main() 设置在 0x10d155d ./main.go:10

(dlv) 断点

断点 runtime-fatal-throw at 0x10388c0 for runtime.fatalthrow() /usr/local/go/src/runtime/panic.go:1162 (0)

runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1189 (0) 的断点未恢复-恐慌在 0x1038940

打印 runtime.curg._panic.arg

main.main() ./main.go:8 (0) 的 0x10d1472 处的断点 1

main.main() ./main.go:9 (0) 的 0x10d154a 处的断点 2

main.main() ./main.go:10 (0) 的断点 3 在 0x10d155d

(dlv) 清除

断点 1 在 0x10d1472 清除 main.main() ./main.go:8

断点 2 在 0x10d154a 清除 main.main() ./main.go:9

断点 3 在 0x10d155d 清除 main.main() ./main.go:10

在上面的示例中,我们在 第 8、9 和 10 行创建了 3 个断点。我们显示所有断点,然后我们一次清除所有断点。当您想一次清理所有断点并移至调试同一程序的另一个区域时,这可能非常方便。

使用 Delve 运行和导航程序

一旦我们设置了所有断点并且我们能够使用列表检查源代码的任何部分,我们现在可以看到我们如何实际“调试”并使用一组非常强大的命令在调试模式下运行我们的程序。

继续

运行程序直到下一个断点或程序终止

(dlv) 中断 ./main.go:10

断点 1 设置为 main.main() 的 0x10d155d ./main.go:10

(dlv) 继续

main.main() ./main.go:10 (命中 goroutine(1):1 总计:1) (PC: 0x10d155d)

5: var m u003d make(map[int]int, 0)

6:

7: 函数 main() {

8: 对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

9: x :u003d fib(n)

\u003d> 10: fmt.Println(n, "fib", x)

11:}

12:}

13:

14: func fib(n int) int {

15: 如果 n < 2 {

在 main.go 文件的第 10 行设置断点后,我们可以运行 continue 并且我们的调试器将运行程序直到下一个断点,在我们的例子中就是第 10 行的断点 1。这一点我们可以做很多事情,比如检查和改变变量的内容。但首先让我们看看我们可以使用哪些其他命令来导航 Go 程序。

下一个

转到下一个源代码行

(dlv) 下一个

5 谎言 5

main.main() ./main.go:8 (PC: 0x10d1693)

3:导入“fmt”

4:

5: var m u003d make(map[int]int, 0)

6:

7: 函数 main() {

\u003d> 8: 对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

9: x :u003d fib(n)

10: fmt.Println(n, "fib", x)

11:}

12:}

13:

就如此容易! next 命令只允许我们按照源代码中指定的时间一次执行一条指令,而不管是否有断点。如果您想逐步分析程序,这非常有用

Step 或者我喜欢称之为“step in”用于告诉调试器进入函数调用,就像 next,但在遇到函数调用时更深入。

(dlv) 下一个

main.main() ./main.go:9 (PC: 0x10d154a)

4:

5: var m u003d make(map[int]int, 0)

6:

7: 函数 main() {

8: 对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

\u003d> 9: x :u003d fib(n)

10: fmt.Println(n, "fib", x)

11:}

12:}

13:

14: func fib(n int) int {

(dlv) 步骤

main.fib() ./main.go:14 (PC: 0x10d1713)

9: x :u003d fib(n)

10: fmt.Println(n, "fib", x)

11:}

12:}

13:

\u003d> 14: func fib(n int) int {

15: 如果 n < 2 {

16: 返回 n

17:}

18:

19: var f int

使用 step 我们可以进入 inside 一个函数定义,而不是仅仅计算它的值并继续前进。当遵循返回我们想要调查其性质和起源的结果的多个函数调用的逻辑时,这非常有用。在不是函数调用的行上使用 step 时,它的行为就像 next delve 指令。它逐行进行。

步出

我之所以喜欢称 step, stepin 是因为它的对应物 stepout 与 step 正好相反。它让我们回到了我们所在函数的调用者。

(dlv) 步出

main.main() ./main.go:9 (PC: 0x10d1553)

返回值:

~r1: 1

4:

5: var m u003d make(map[int]int, 0)

6:

7: 函数 main() {

8: 对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

\u003d> 9: x :u003d fib(n)

10: fmt.Println(n, "fib", x)

11:}

12:}

13:

14: func fib(n int) int {

重新开始

重新启动将允许我们重新启动程序,以防程序终止并且我们仍想调试。如果我们不想丢失所有断点并且不想退出并从头开始创建新的 delve 调试服务器,这将特别有用

(dlv) 清除

断点 4 在 0x10d155d 清除 main.main() ./main.go:10

(dlv) 继续

1个谎言1

9 纤维 34

98 纤维6174643828739884737

6 纤维 8

进程 39014 已退出,状态为 0

(dlv) 重启

进程以 PID 39050 重新启动

在我们的示例中,我们只是清理所有断点,继续,以便程序一直运行到终止并再次重新启动进程。现在我们可以重新开始调试,而无需从头开始重新调试。

如何用 Delve 查看程序变量

到目前为止,我们已经了解了如何添加和管理断点,如何使用 delve 在程序中轻松导航。现在我们只需要能够查看和编辑程序变量和内存,这是调试过程的基本部分。有很多非常有用的 delve 命令可以用于此目的。

打印

打印是最简单的一种,它允许我们查看变量内容并评估表达式

(dlv) 中断 ./main.go:10

断点 1 设置为 main.main() 的 0x10d155d ./main.go:10

(dlv) 继续

main.main() ./main.go:10 (命中 goroutine(1):1 总计:1) (PC: 0x10d155d)

5: var m u003d make(map[int]int, 0)

6:

7: 函数 main() {

8: 对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

9: x :u003d fib(n)

\u003d> 10: fmt.Println(n, "fib", x)

11:}

12:}

13:

14: func fib(n int) int {

15: 如果 n < 2 {

(dlv) 打印 x

5

在上面的示例中,我们刚刚在第 10 行将断点设置为 main.go,并打印了变量 x 的值,它是位置 5 处序列的斐波那契值,如上面的代码所示。

您现在可以尝试在 fib 函数内部导航并尝试打印各种值,例如 n 或 m 映射变量!

当地人

locals 命令对于调查所有局部变量的内容非常有用

(dlv) 清单

main.main() ./main.go:10 (命中 goroutine(1):1 总计:1) (PC: 0x10d155d)

5: var m u003d make(map[int]int, 0)

6:

7: 函数 main() {

8: 对于 _, n :u003d 范围 []int{5, 1, 9, 98, 6} {

9: x :u003d fib(n)

\u003d> 10: fmt.Println(n, "fib", x)

11:}

12:}

13:

14: func fib(n int) int {

15: 如果 n < 2 {

(dlv) 当地人

n u003d 5

x u003d 5

结论

这组命令应该足以让您放心地去调试您的 Go 应用程序。 Go delve 调试器也可用于所有主要的 Go 编辑器和 IDE,您可以在此处查看可用集成列表https://github.com/go-delve/delve/blob/master/Documentation/EditorIntegration.md。

如果您掌握了 Go Delve 命令行调试器的使用,那么使用遵循相同概念和结构的其他编辑器集成版本将更加容易。

我计划发布专门用于 goroutine 调试的 Go Delve 调试指南的第二部分。感谢阅读,我希望你喜欢这个内容!

在此处查看有关 Delve 的视频教程

https://youtu.be/a1SneuI65O0

想加入最好的 Go (Golang) 开发者社区吗?免费加入 Golang Cafe 开发者名录