使用 Delve 工具调试 Golang 程序

2,512 阅读7分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战

Delve是一个go语言的第三方调试器,github地址是: github.com/go-delve/de… 。 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/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命令在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

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