译文 使用 Delve 工具调试 Golang 程序

yudotyang · 2021年05月18日 · 314 次阅读

Delve 是一个 go 语言的第三方调试器,github 地址是: https://github.com/go-delve/delve 。 Delve 是 GDB 调试器的有效替代品。与 GDB 相比,它能更高的理解 Go 的运行时,数据结构以及表达式。Delve 目前支持 Linux,OSX 以及 Windows 的 amd64 平台。

本文主要介绍使用 delve 调试器如何调试 Go 程序。内容包含如下:

  • 下载安装 Delve
  • 查看源码命令组,
  • 添加及管理断点命令组(添加断点、查看断点、清除断点等)
  • 控制程序的执行流程命令组
  • 查看当前状态的变量及表达式的值命令组。

阅读完本文后,你将能够使用 Delve 工具很容易的调试你的 go 程序。

下载并安装 Go Delve

  • 安装方式一: $ git clone https://github.com/go-delve/delve $ cd delve $ go install github.com/go-delve/delve/cmd/dlv
  • 安装方式二:(适用于 Go 1.16 及以后版本) $ go install github.com/go-delve/delve/cmd/dlv@latest 执行完命令后,dlv 命令被安装在 $GOPATH/bin 目录下。如果没有设置 $GOPATH,则被默认安装在 $HOME/go/bin 目录下。

输入 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)

Delve 客户端

通过执行 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,该命令允许我们查看给定行的源代码。我们可以通过包名 + 函数名、文件名 + 行数方式指定要查看的源文件

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

根据正则匹配对应的函数列表。一般用于搜索函数

funcs [<regex\>]

例如:

(dlv) funcs fib
main.fib

Exit

退出当前调试会话命令

(dlv) exit

添加及管理断点命令组

一旦你知道用 list 命令如何显示源代码片段后,你就可以开始在程序的相应位置增加断点来调试程序了。

假设,我们想在 main.go 文件中的第 10 行增加一个端点,那么,我们就可以使用 break 命令来达到设置断点的目的。

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

设置完断点后,接下来需要查看设置了哪些断点。则需要使用 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 行设置的断点。

clear

删除特定的断点。指定断点名或断点标识 ID

clear <breakpoint name or id\>

该命令一般用于要移除错误设置的标识,或者想移除原有标识并设置新的标识时使用。

例如,下面的例子中,删除标识 ID 为 1 的断点。标识号是使用 breakpoints 命令显示出来的 ID。

(dlv) clear 1
Breakpoint 1 cleared at 0x10d155d for main.main() ./main.go:10

clearall

清除所有手动增加的断点

例如,在下面的例子中,我们在第 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 命令检查源代码,现在我们看下如何运行程序。

continue

运行程序,直到遇到下一个断点或者直到程序结束。 例如下面例子,我们在 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 {

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:

### 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 {

restart

该命令允许我们在程序终止或重新开始调试程序的时候,重启该程序,同时保留住之前所有设置过的断点。即之前设置过的断点不会丢失。

查看当前状态的变量及表达式的值命令组

到目前为止,我们已经知道了如何添加并管理断点,如何控制程序的执行流程。现在,我们介绍如何查看、编辑程序变量和内存数据,这也是调试中最基础的部分。

print

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

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 组相关的命令:

  • 查看源代码命令:list、func 以及推出 exit
  • 添加断点以及管理断点:break、breakpoints、clear、clearall
  • 控制程序流程的命令:continue、next、step、stepout、restart
  • 查看当前变量的命令:print、locals

通过以上命令,通过查看源码,设置断点、执行到断点、输出当前的变量状态,满足了最基本的程序执行的需要。

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册