Delve 是一个 go 语言的第三方调试器,github 地址是: https://github.com/go-delve/delve 。Delve 是 GDB 调试器的有效替代品。与 GDB 相比,它能更高的理解 Go 的运行时,数据结构以及表达式。Delve 目前支持 Linux,OSX 以及 Windows 的 amd64 平台。
本文主要介绍使用 delve 调试器如何调试 Go 程序。内容包含如下:
阅读完本文后,你将能够使用 Delve 工具很容易的调试你的 go 程序。
输入 dlv 命令检查是否安装成功:
$ dlv version
Delve Debugger
Version: 1.6.0
Build $Id: 8cc9751909843dd55a46e8ea2a561544f70db34d $
如果在终端输入 dlv 命令提示找不到该命令,则将 GOPATH/bin 下的 dlv 命令软链接到/usr/local/bin 目录。即保证 dlv 命令在 PATH 环境变量下。
首先我们通过下面的斐波那契数列函数代码作为示例讲解。
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
}
在当前目录下输入 dlv debug 命令,编译并启动一个调试会话。
$ dlv debug main.go
Type 'help' for list of commands.
(dlv)
通过执行 dlv debug 即开启了一个调试的会话。dlv 编译程序并附加到二进制中,接下来我们可以开始调试我们的程序了。通过 dlv debug 命令,我们即开启了一个 delve 的解释器,我们可以称它为 Delve 客户端,由这个客户端发送调试的命令到 delve 的服务端。
在开启的 delve 客户端下,我们输入 help 命令,可以查看所有可用的子命令。如下:
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. //重启动该调试进程,会保留之前设置的所有断点。和rebuild的区别是,restart命令不会重新编译源文件,即在调试过程中,如果源文件有变更,restart命令后不会体现。
step (alias: s) ------------- Single step through program. // 单步执行。如果遇到函数调用,则进入到被调用的函数中。和next的区别是,当next遇到函数调用时,不进入函数内部,仍留在主函数中。具体例子中会讲解。
step-instruction (alias: si) Single step a single cpu instruction. //单步执行cpu指令。
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. //设置trace断点
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.
我们要介绍的第一个命令是 list,该命令允许我们查看给定行的源代码。我们可以通过包名 + 函数名、文件名 + 行数方式指定要查看的源文件
[goroutine <n>] [frame <m>] list [<linespec>]
如下所示,通过包名 + 函数名的方式查看源代码:
(dlv) list main.main
Show /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
Show /workspace/tutorials/delve/main.go:7(PC:0x10d145b)
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 [<regex\>]
例如:
(dlv) funcs fib
main.fib
退出当前调试会话命令
(dlv) exit
一旦你知道用 list 命令如何显示源代码片段后,你就可以开始在程序的相应位置增加断点来调试程序了。
假设,我们想在 main.go 文件中的第 10 行增加一个端点,那么,我们就可以使用 break 命令来达到设置断点的目的。
设置一个端点。其中 name 指的是给断点起一个名称,linespec 用来指定在设置断点的具体位置
break [name] <linespec\>
例如,我们在 main.go 文件的第 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 个断点。其中,前两个断点是 dlv 自动加的,以便当遇到错误或 panics 时可以查看程序的状态以及变量的信息。
第 3 个断点就是我们刚才手动在第 10 行设置的断点。
删除特定的断点。指定断点名或断点标识 ID
clear <breakpoint name or id\>
该命令一般用于要移除错误设置的标识,或者想移除原有标识并设置新的标识时使用。
例如,下面的例子中,删除标识 ID 为 1 的断点。标识号是使用 breakpoints 命令显示出来的 ID。
(dlv) clear 1
Breakpoint 1 cleared at 0x10d155d for main.main() ./main.go:10
清除所有手动增加的断点
例如,在下面的例子中,我们在第 8、9、10 行设置 3 个断点。然后使用 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
一旦我们可以设置断点,并且能够通过 list 命令检查源代码,现在我们看下如何运行程序。
运行程序,直到遇到下一个断点或者直到程序结束。例如下面例子,我们在 main.go 文件的第 10 行设置一个端点,然后使用 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 {
运行到源代码的下一行。该命令在我们想对程序一步一步调试的时候非常有用。例如,下面程序就是从断点开始,每次往前执行一行,无论下面有没有断点,每次都只运行一行。
(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:
### step step 命令用于告诉调试器进入到函数调用的内部,和 next 类似,但是当遇到函数调用时,step 命令会进入到被调用函数的内部,而 next 则将函数调用看成是一个语句。例如,下面示例中,当执行到底 9 行的时候,next 则会计算 fib 函数的值,并进入到第 10 行。但 step 则会从第 9 行,直接进入到第 14 行的函数定义,然后逐步执行。
```golang (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
```
### stepout 和 step 相对应,是 step 的反向操作。从被调用函数中返回调用函数。例如,如下示例中,会返回到第 9 行。
(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 {
该命令允许我们在程序终止或重新开始调试程序的时候,重启该程序,同时保留住之前所有设置过的断点。即之前设置过的断点不会丢失。
到目前为止,我们已经知道了如何添加并管理断点,如何控制程序的执行流程。现在,我们介绍如何查看、编辑程序变量和内存数据,这也是调试中最基础的部分。
Print 是最简单的查看变量内容和表达式的命令。例如如下,我们在文件的第 10 行设置了断点,然后用 continue 执行到断点处,然后使用 print 命令打印 x 变量的值,如下:
(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
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
本文中,我们介绍了 4 组相关的命令:
通过以上命令,通过查看源码,设置断点、执行到断点、输出当前的变量状态,满足了最基本的程序执行的需要。