使用 Delve 进行 Golang 调试
在本文中,我们将了解如何使用 Delve 调试 Go (Golang) 程序。 Delve
是 Go 编程语言的第三方调试器,可在 github https://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 文档。
Objective
到本文结束时,大家将能够使用 delve 调试器命令行工具轻松调试和检查 Go 程序。 我们将看到如何在 Go 程序中查看、添加和更改断点,逐行或通过断点导航程序,检查变量、函数和表达式值,最后详细分析我们所有的程序。
下载并安装 Go Delve
只需使用 go get
命令即可下载并安装 Go delve
Linux, Windows, OSX
$ go get github.com/go-delve/delve/cmd/dlv
如果我们使用的是 Go 模块,我们可能希望在项目目录外执行此命令,以避免将 delve 添加为 go.mod 文件中的依赖项
如果我们有一个有效的 Go 安装,则应该已经设置了以下内容:
-
确保
GOBIN
env 变量设置正确,这将指示将存储dlv
(delve)命令的目录。 我们可以通过输入go env GOBIN
进行检查 - 确保 PATH 包含 GOBIN 这将允许我们在不指定绝对路径的情况下运行 Go 二进制可执行文件
OSX
在 mac OS 中,我们可能还需要通过键入以下内容来启用开发人员工具
$ xcode-select --install
检查我们的安装
完成安装后,我们可以通过检查版本来检查是否已成功安装 delve
$ dlv version
Delve Debugger
Version: 1.5.1
Build: $Id: bca418ea7ae2a4dcda985e623625da727d4525b5 $
这意味着你已经成功安装了 delve。 现在让我们开始调试吧。
开始调试(Delve 服务器)
当我们开始调试时,我们通常的意思是,我们开始一个“调试会话”。 要启动调试会话,我们可以使用 dlv
命令行帮助中提供的命令之一
$ dlv help
...
Usage:
dlv [command]
Available Commands:
attach Attach to running process and begin debugging.
connect Connect to a headless debug server.
core Examine a core dump.
dap [EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
debug Compile and begin debugging main package in current directory, or the package specified.
exec Execute a precompiled binary, and begin a debug session.
help Help about any command
run Deprecated command. Use 'debug' instead.
test Compile test binary and begin debugging program.
trace Compile and begin tracing program.
version Prints version.
我们感兴趣的命令是 dlv debug
和 dlv exec
,它们都用于启动调试会话,唯一的区别是一个(dlv debug)也从源代码编译二进制文件,另一个(dlv exec)期望有 编译好的二进制文件。
如果我们想要调试 Go 测试,test 命令也很有用。
Golang 调试代码示例
下面的代码片段代表我们将用于调试会话的代码示例,它是一个斐波那契实现。
package main
import "fmt"
var m = make(map[int]int, 0)
func main() {
for _, n := range []int{5, 1, 9, 98, 6} {
x := fib(n)
fmt.Println(n, "fib", x)
}
}
func fib(n int) int {
if n < 2 {
return 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 debug main.go
Type 'help' for list of commands.
(dlv)
这将启动调试会话。 这也被称为 delve server
,因为它是一个正在运行的等待指令的进程。
深入理解客户端
一旦我们的调试会话开始,我们已经编译并附加到一个 Go 二进制文件,我们可以开始调试我们的程序。 我们现在看到一个新的 repl,它只是一个 delve 解释器,我们也可以将其称为“delve 客户端”,它将调试指令发送到我们之前创建的 delve 服务器。
我们可以通过键入以下内容来查看所有可用的命令。
Type 'help' for list of commands.
(dlv) help
The following commands are available:
Running the program:
call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!)
continue (alias: c) --------- Run until breakpoint or program termination.
next (alias: n) ------------- Step over to next source line.
rebuild --------------------- Rebuild the target executable and restarts it. It does not work if the executable was not built by delve.
restart (alias: r) ---------- Restart process.
step (alias: s) ------------- Single step through program.
step-instruction (alias: si) Single step a single cpu instruction.
stepout (alias: so) --------- Step out of the current function.
Manipulating breakpoints:
break (alias: b) ------- Sets a breakpoint.
breakpoints (alias: bp) Print out info for active breakpoints.
clear ------------------ Deletes breakpoint.
clearall --------------- Deletes multiple breakpoints.
condition (alias: cond) Set breakpoint condition.
on --------------------- Executes a command when a breakpoint is hit.
trace (alias: t) ------- Set tracepoint.
Viewing program variables and memory:
args ----------------- Print function arguments.
display -------------- Print value of an expression every time the program stops.
examinemem (alias: x) Examine memory:
locals --------------- Print local variables.
print (alias: p) ----- Evaluate an expression.
regs ----------------- Print contents of CPU registers.
set ------------------ Changes the value of a variable.
vars ----------------- Print package variables.
whatis --------------- Prints type of an expression.
Listing and switching between threads and goroutines:
goroutine (alias: gr) -- Shows or changes current goroutine
goroutines (alias: grs) List program goroutines.
thread (alias: tr) ----- Switch to the specified thread.
threads ---------------- Print out info for every traced thread.
Viewing the call stack and selecting frames:
deferred --------- Executes command in the context of a deferred call.
down ------------- Move the current frame down.
frame ------------ Set the current frame, or execute command on a different frame.
stack (alias: bt) Print stack trace.
up --------------- Move the current frame up.
Other commands:
config --------------------- Changes configuration parameters.
disassemble (alias: disass) Disassembler.
edit (alias: ed) ----------- Open where you are in $DELVE_EDITOR or $EDITOR
exit (alias: quit | q) ----- Exit the debugger.
funcs ---------------------- Print list of functions.
help (alias: h) ------------ Prints the help message.
libraries ------------------ List loaded dynamic libraries
list (alias: ls | l) ------- Show source code.
source --------------------- Executes a file containing a list of delve commands
sources -------------------- Print list of source files.
types ---------------------- Print list of types
Type help followed by a command for full documentation.
有一些可用的命令,但我们可以轻松地将它们分成不同的重点区域,以更好地了解如何接近 delve 客户端。
通用 Delve 命令
我要提请大家注意的第一个命令是 list
命令,它允许我们列出给定位置的源代码。 我们可以通过传递包名和函数或文件路径和一行来指定位置。 例如
list
按包和函数名称显示源代码
(dlv) list main.main
Showing /workspace/tutorials/delve/main.go:7 (PC: 0x10d145b)
2:
3: import "fmt"
4:
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
(dlv)
按文件名和行号显示源代码
(dlv) list ./main.go:14
Showing /workspace/tutorials/delve/main.go:14 (PC: 0x10d1713)
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
16: return n
17: }
18:
19: var f int
(dlv)
还有一个命令可以搜索给定模式的函数
Funcs
(dlv) funcs fib
main.fib
Exit
如果你卡在调试会话中,你可以退出
(dlv) exit
使用 Delve 添加断点
一旦我们知道如何使用 delve list
命令在屏幕上显示源代码的一部分,我们可能想要开始在我们的程序中添加断点,在我们想要停止并检查变量和其他表达式值的区域。
对于这个基本示例,假设我们想在 main.go 文件的第 10 行添加一个断点,我们已经在前面的列表示例中看到过。 这将通过使用 break
关键字后跟要添加断点的位置来完成。
break
这将在指定位置添加一个断点,并列出第 10 行将使用该断点的位置
(dlv) break ./main.go:10
Breakpoint 1 set at 0x10d155d for main.main() ./main.go:10
(dlv) list ./main.go:10
Showing /workspace/tutorials/delve/main.go:10 (PC: 0x10d155d)
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
(dlv)
breakpoints
列出此调试会话的所有当前断点
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x10388c0 for runtime.fatalthrow() /usr/local/go/src/runtime/panic.go:1162 (0)
Breakpoint unrecovered-panic at 0x1038940 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1189 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x10d155d for main.main() ./main.go:10 (0)
在此示例中,我们可以看到 3 个断点。 前两个由 delve 自动添加并用作恐慌和致命错误的保护措施,以便我们能够查明程序的状态并检查变量、堆栈跟踪和状态。
第三个断点即断点 1 是我们在第 10 行添加的断点。
尝试添加新断点,然后查看如何显示在此处的列表中!
clear
从调试会话中删除特定断点
(dlv) clear 1
Breakpoint 1 cleared at 0x10d155d for main.main() ./main.go:10
如果我们想删除错误添加的特定断点,或者只是因为我们需要删除并启动同一程序的其他区域,这将很有用。
clearall
清除所有手动添加的断点
(dlv) break ./main.go:8
Breakpoint 1 set at 0x10d1472 for main.main() ./main.go:8
(dlv) break ./main.go:9
Breakpoint 2 set at 0x10d154a for main.main() ./main.go:9
(dlv) break ./main.go:10
Breakpoint 3 set at 0x10d155d for main.main() ./main.go:10
(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x10388c0 for runtime.fatalthrow() /usr/local/go/src/runtime/panic.go:1162 (0)
Breakpoint unrecovered-panic at 0x1038940 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1189 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x10d1472 for main.main() ./main.go:8 (0)
Breakpoint 2 at 0x10d154a for main.main() ./main.go:9 (0)
Breakpoint 3 at 0x10d155d for main.main() ./main.go:10 (0)
(dlv) clearall
Breakpoint 1 cleared at 0x10d1472 for main.main() ./main.go:8
Breakpoint 2 cleared at 0x10d154a for main.main() ./main.go:9
Breakpoint 3 cleared at 0x10d155d for main.main() ./main.go:10
在上面的示例中,我们在第 8、9 和 10 行创建了 3 个断点。我们显示所有断点,然后立即清除所有断点。 当我们想一次清除所有断点并移动到调试同一程序的另一个区域时,这会非常方便。
使用 Delve 运行和导航程序
一旦我们设置了所有断点并且我们能够使用列表检查源代码的任何部分,我们现在可以看到我们如何通过使用一组非常强大的命令实际“调试”并在调试模式下运行我们的程序。
continue
运行程序直到下一个断点或直到程序终止
(dlv) break ./main.go:10
Breakpoint 1 set at 0x10d155d for main.main() ./main.go:10
(dlv) continue
> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10d155d)
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
=> 10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
在 main.go 文件的第 10 行设置断点后,我们可以继续运行,我们的调试器将运行程序直到下一个断点,在我们的例子中就是第 10 行的断点 1。此时我们可以 做很多事情,比如检查和更改变量内容。 但首先让我们看看我们可以使用哪些其他命令来导航我们的 Go 程序。
next
转到下一个源代码行
(dlv) next
5 fib 5
> main.main() ./main.go:8 (PC: 0x10d1693)
3: import "fmt"
4:
5: var m = make(map[int]int, 0)
6:
7: func main() {
=> 8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
就如此容易! next
命令只允许我们按照源代码中指定的方式一次执行一条指令,而不管是否有断点。 如果我们想逐步分析程序,这将非常有用
step
step 或我喜欢称之为 step in
用于告诉调试器进入函数调用内部,就像 next 一样,但用于在遇到函数调用时更深入。
(dlv) next
> main.main() ./main.go:9 (PC: 0x10d154a)
4:
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
=> 9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
(dlv) step
> main.fib() ./main.go:14 (PC: 0x10d1713)
9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
=> 14: func fib(n int) int {
15: if n < 2 {
16: return n
17: }
18:
19: var f int
使用 step
我们可以进入函数定义,而不是仅仅计算它的值并继续。 当遵循返回结果的多个函数调用的逻辑时,这非常有用,我们想要调查其性质和起源。 当在不是函数调用的行上使用 step
时,它的行为就像下一个 delve
指令一样。 它逐行进行。
stepout
之所以喜欢把 step 叫做 stepin,是因为它的对应,stepout 正好是 step 的反义词。 它把我们带回到我们所在函数的调用者。
(dlv) stepout
> main.main() ./main.go:9 (PC: 0x10d1553)
Values returned:
~r1: 1
4:
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
=> 9: x := fib(n)
10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
restart
restart
将允许我们重新启动程序,以防程序终止但我们仍想调试。 如果我们不想丢失所有断点并且不想退出并从头开始创建新的 delve
调试服务器,这将特别有用
(dlv) clearall
Breakpoint 4 cleared at 0x10d155d for main.main() ./main.go:10
(dlv) continue
1 fib 1
9 fib 34
98 fib 6174643828739884737
6 fib 8
Process 39014 has exited with status 0
(dlv) restart
Process restarted with PID 39050
在我们的示例中,我们只是清除所有断点,继续以便程序一直运行到终止并再次重新启动进程。 现在我们可以重新开始调试,而不必从头开始调试。
如何使用 Delve 查看程序变量
到目前为止,我们已经了解了如何添加和管理断点,如何使用 delve
在程序中的任何地方轻松导航。 现在我们只需要能够查看和编辑程序变量和内存,这是调试过程的基本部分。 有许多非常有用的 delve 命令可用于此目的。
print
是最简单的一种,它允许我们查看变量内容并评估表达式
(dlv) break ./main.go:10
Breakpoint 1 set at 0x10d155d for main.main() ./main.go:10
(dlv) continue
> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10d155d)
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
=> 10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
(dlv) print x
5
在上面的示例中,我们刚刚在第 10 行将断点设置到我们的 main.go 并打印了变量 x 的值,它是上述代码中位置 5 处序列的斐波那契值。
我们现在可以尝试在
fib
函数内部导航并尝试打印各种值,例如 n 或 m 映射变量!
locals
locals
命令对于调查所有局部变量的内容非常有用
(dlv) list
> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10d155d)
5: var m = make(map[int]int, 0)
6:
7: func main() {
8: for _, n := range []int{5, 1, 9, 98, 6} {
9: x := fib(n)
=> 10: fmt.Println(n, "fib", x)
11: }
12: }
13:
14: func fib(n int) int {
15: if n < 2 {
(dlv) locals
n = 5
x = 5
总结
这组命令应该足以让我们自信地调试我们的 Go 应用程序。 Go delve
调试器也可用于所有主要的 Go 编辑器和 IDE,我们可以在此处查看可用集成列表 https://github.com/go-delve/delve/blob/master/Documentation/EditorIntegration.md
。
如果我们掌握了 Go Delve
命令行调试器的使用,那么使用遵循相同概念和结构的其他编辑器集成版本将会更加容易。
相关文章
在 Golang 中使用 If-Else 和 Switch Loop Inside HTML 模板
发布时间:2023/04/27 浏览次数:65 分类:Go
-
本篇文章介绍了在 Golang 的 HTML 模板中使用 if-else 和 switch 循环。因此,只要输出是 HTML,就应该始终使用 HTML 模板包而不是文本模板。
Golang 中的零值 Nil
发布时间:2023/04/27 浏览次数:166 分类:Go
-
本篇文章介绍 nil 在 Golang 中的含义,nil 是 Go 编程语言中的零值,是众所周知且重要的预定义标识符。
Golang 中的 Lambda 表达式
发布时间:2023/04/27 浏览次数:93 分类:Go
-
本篇文章介绍如何在 Golang 中创建 lambda 表达式。Lambda 表达式似乎不存在于 Golang 中。 函数文字、lambda 函数或闭包是匿名函数的另一个名称。
在 Go 中使用断言
发布时间:2023/04/27 浏览次数:181 分类:Go
-
本篇文章介绍了 assert 在 GoLang 中的使用。在 Go 语言中使用断言:GoLang 不提供对断言的任何内置支持,但我们可以使用来自 Testify API 的广泛使用的第三方包断言。
Go 中的随机数生成
发布时间:2023/04/27 浏览次数:114 分类:Go
-
本篇文章介绍如何在 Go 语言中使用随机数生成功能。Go 中的随机数生成 Go 语言为随机数生成功能提供内置支持。 内置包 math 有方法 rand(),用于随机数生成。
在 Go 中使用 Electron API 创建 GUI
发布时间:2023/04/27 浏览次数:124 分类:Go
-
本篇文章介绍如何在 Go 语言中使用 Electron API 创建 GUI。Electron API 或 Astilectron 用于为 GoLang 创建 GUI。
在 GoLang 中安装包
发布时间:2023/04/27 浏览次数:122 分类:Go
-
使用 Go 语言的 get 命令安装所需的包非常容易。 Go 语言提供了多种命令来执行某些任务,get 就是其中之一。