Delve是Go官方推荐的调试器,我们熟知的Goland、LiteIDE都集成了Delve,同时各大通用IDE如VSCode、Atom、Sublime等的Go插件也都集成了Delve调试Go代码。

除了IDE以外,还有一个使用Delve的GUI调试器——Gdlv。除了提供图形界面外,Gdlv也可以执行Delve命令。

开始

我们需要安装Delve和Gdlv。
Go1.16及以上版本:

$ go install github.com/go-delve/delve/cmd/dlv@latest
$ go install github.com/aarzilli/gdlv@latest

Go1.16以下版本

$ go get -u github.com/derekparker/delve/cmd/dlv
$ go get -u github.com/aarzilli/gdlv

确保的你Go bin目录已添加到path环境变量。
查看是否安装成功:

$ dlv version
$ gdlv version

配置存储路径

Delve会在磁盘存储一个配置文件和一个历史命令文件。默认情况下,Linux系统是放在$HOME/.config/dlv下,其他系统包括Windows是在$HOME/.dlv下,$HOME就是你的用户目录。

可以通过配置XDG_CONFIG_HOME环境变量来更改存放目录。

启动调试

源代码调式

打开命令行切换到main包目录,然后执行以下命令。

$ gdlv debug

也可以手动指定源码目录。

$ gdlv -d 源码目录 debug

可执行文件调试

Delve也可以调试编译好的可执行文件,当然也是由Delve编译的调试版可执行文件。

$ gdlv exec xxx.debug.exe

exec不支持-d选项。

调试Go测试代码

同调试源代码基本一致,只是将debug换成了test

$ gdlv test

也可通过-d选项指定代码目录。

打开调试窗口以后默认断点在main函数处,在Command窗口底部可以输入Delve命令。

go语言ide开发工具调试 go语言调试器_go调试

Gdlv启动调试默认会停在main函数处,但是Delve并不会停在main处,所以在直接用dlv debug命令调试时,需要先在main函数处设置断点,然后运行至断点处,具体命令见后面章节。

Gdlv界面操作

设置断点

在Gdlv的Listing窗口,右键会弹出一个菜单,一个是设置断点,一个是运行到指定行。

go语言ide开发工具调试 go语言调试器_dlv_02


右键断点,可以禁用/启动断点,编辑断点,以及运行到断点处。

go语言ide开发工具调试 go语言调试器_Gdlv_03


编辑断点可以设置断点条件。

go语言ide开发工具调试 go语言调试器_Gdlv_04

单步

在Command窗口的顶部有运行、单步、跳入跳出按钮。

go语言ide开发工具调试 go语言调试器_go语言ide开发工具调试_05


右上角的NEW WINDOW下拉框可以打开更多窗口,查看其他信息。比如选则Breakpoints可以查看所有断点。

查看变量

Variables窗口可以查看本地变量,通过Filter可以过滤变量。Full Types单选框可以显示完整类型信息,Address单选框可以显示变量地址。

go语言ide开发工具调试 go语言调试器_Gdlv_06

Delve命令

接下来要介绍的是Devle的调试命令,我们可以在Gdlv的Command窗口输入这些命令。

断点

设置断点

设置断点的命令是break <loction>break的别名是b,因此也可以使用b <location>
此外断点也可以命名,格式为break 断点名 <location>,或者b 断点名 <location>
location有五种格式。

内存地址

支持十进制,十六进制和八进制。

文件名:行号

文件名可以是相对路径,如果没有歧义,也可以只写文件名。如果省略文件名,则表示当前文件。

b main.go:8
b 8

偏移量

基于当前行上下偏移。

b +1
b -2

函数:行号

在不引起歧义的情况下,可以直接写函数名,否则应该写包名.函数名。这里的行号表示的是基于函数的偏移行数,如果省略,表示第0行,也就是func xxx那行。

b main.max
b main.max:2

正则
在所有满足正则匹配的函数处设置断点。正则表达式需用/包裹:/正则表达式/

查看断点

breakpoints [-a]或者简写bp [-a]。通过-a选项可以查看所有物理断点,包括内部断点。
这个命令在Gdlv中不可用,可以通过点击NEW WINDOW->Breakpoints查看断点。

清除断点

单个清除

命令格式为clear <id or name>

当我们通过break命令设置断点时,会打印出断点ID。

go语言ide开发工具调试 go语言调试器_go语言ide开发工具调试_07


如果设置断点时指定了名称,那么也可以通过断点名删除。

通过这个命令删除断点不会在Gdlv中实时显示,需要做一个别的操作刷新页面,删除的断点才会消失。

批量清除

命令为clearall [<locspec>],如果省略<location>则清除所有断点,否则,删除与<location>匹配处的断点。
这个命令在Gdlv中也不可用。

断点条件

设置断点条件的命令是condition,别名是cond。这个命令在Gdlv中也不可用,所以在Gdlv中只能通过UI来设置断点条件。条件是一个布尔表达式,关于Delve表达式见表达式。

满足条件进入断点

命令格式如下:

cond <id or name> expr

进入断点次数做为条件

除了普通的布尔表达式,也可以根据进入断点的次数决定此次进入断点是否要停留。支持以下几种表达式。

cond -hitcount bp > n
cond -hitcount bp >= n
cond -hitcount bp < n
cond -hitcount bp <= n
cond -hitcount bp == n
cond -hitcount bp != n
cond -hitcount bp % n

bp是内置变量,表示进入断点次数,n是条件。

清除条件

通过-clear选项可以清除某个断点上的条件。

cond -clear <id or name>

断点回调

进入断点时执行命令。格式为:

on <id or name> <command>

支持的<command>有以下5个:

  1. print 打印表达式
  2. stack 打印调用栈
  3. goroutine 显示goroutine
  4. trace 将断点变成跟踪点(trace point)
  5. cond 等同于cond

修改命令使用-edit选项。

on <id or name> -edit

禁用/启动断点

禁用/启用断点可以通过Gdlv界面进行操作。Delve官方文档给了一个toggle命令来禁用/启用断点,但是实测会提示命令不可用,不知道是不是版本的原因。

运行调试

单步

next [n]

Delve中,这个next命令可以指定执行几行代码。但在Gdlv中,n会被忽略,始终执行1行。

单步进入

命令为step,别名为s

单步跳出

命令为stepout,别名为so
在Gdlv中不支持so

单步cpu指令

命令为step-instruction,别名为si。CPU指令级别的单步。

重新开始

命令restart,别名r

退出调试

命令:exit

继续执行

命令为continue,别名为c

默认执行到下一个断点或至程序结束。也可以指定location,运行到指定位置。

c <loccation>

在Gdlv中,<location>会被忽略。

查看状态

查看函数参数

命令格式:

[goroutine <n>] [frame <m>] args [-v] [<regex>]

打印函数参数,-v选项用于显示参数详细信息,<regexp>可以过滤变量。

args
args a
goroutine 1 args

args在Gdlv中不可用,当进入函数后,参数和本地变量会自动显示在Valriables窗口。

go语言ide开发工具调试 go语言调试器_go语言ide开发工具调试_08

查看本地变量

locals的使用与args一样。

[goroutine <n>] [frame <m>] locals [-v] [<regex>]

locals在Gdlv中也不可用,原因同args

查看包变量

vars [-v] [<regex>]

只打印包级变量。

监控表达式

添加监控表达式

dispaly -a <expr>

<expr>添加到监控列表,每当程序停下时都会打印出监控列表中表达式的值。

(dlv) display -a a+1
0: a+1 = 2

:前面的0是表达式的下标,删除时会用到。

查看监控表达式列表
display不带任何参数会打印出所有被监控的表达式以及表达式的值。

(dlv) display
0: a+1 = 3
1: b = 4

这里也会打印出表达式的编号。

删除表达式监控

display -d <index>

这里的<index>是表达式的编号。

求值表达式

命令为print,别名p。命令格式:

[goroutine <n>] [frame <m>] print <expression>

display不同的是,print只会打印表达式的值1次。

print a
goroutine 1 print b

查看寄存器

regs [-a]

打印CPU寄存器的值,-a选项打印更多寄存器的值。

打印内存

命令examinemem,别名x

x [-fmt <format>] [-count|-len <count>] [-size <size>] <address>

<address>开始按<format>格式以<size>字节为单位打印<count>次。

<format>有5种:

  1. bin 二进制格式
  2. oct 八进制格式
  3. dec 十进制格式
  4. hex 十六进制格式(默认)
  5. addr 地址

<count>表示次数,默认1,最大值1000。

<size>表示字节数,默认1,最大值8。

<address表示内存地址。

示例:

(dlv) x 0xc00010fec8
0xc00010fec8:   0x02
(dlv) x -fmt dec 0xc00010fec8
0xc00010fec8:   002
(dlv) x -fmt dec -size 8 0xc00010fec8
0xc00010fec8:   000000000000000000000002
(dlv) x -fmt dec -count 2 -size 8 0xc00010fec8
0xc00010fec8:   000000000000000000000002   000000000000000000000004

查看表达式类型

whatis <expr>

打印表达式类型。

修改变量值

[goroutine <n>] [frame <m>] set <variable> = <value>

只能修改数字和指针类型的值。

查看文档

查看所有命令文档。

help

查看某个命令文档。

help xxx

表达式

Delve支持的表达式语法是Go语法的子集。

语法

  • <-++--外的单目和双目运算符
  • 比较运算符
  • 数字类型的类型转换
  • 数字和指针类型相互转换
  • string[]byte[]rune相互转换
  • 访问结构体成员(如a.A)
  • 切片和下标运算,支持数组、切片和字符串
  • 访问map
  • 指针解引用
  • 调用内置函数:caplencompleximagreal
  • 类型断言(如somevar.(concretetyp))

特殊变量

  • runtime.curg:当前goroutine的G结构体。如runtime.curg.goid就是当前goroutine的goroutine id。
  • runtime.frameoff:当前栈帧基址距栈底的偏移量。
(dlv) print runtime.curg.goid
1
(dlv) print runtime.frameoff
-312

嵌套深度限制

Delve打印变量时默认会解引用,但是解引用最多解两层,嵌套超过两层的变量会打印出地址。

例如:

(dlv) print c1
main.cstruct {
	pb: *struct main.bstruct {
		a: (*main.astruct)(0xc82000a430),
	},
	sa: []*main.astruct len: 3, cap: 3, [
		*(*main.astruct)(0xc82000a440),
		*(*main.astruct)(0xc82000a450),
		*(*main.astruct)(0xc82000a460),
	],
}

想打印sa的第一个元素,有两种方式:

  1. 下标运算:print c1.sa[0]
  2. 直接用地址:print *(*main.astruct)(0xc82000a440)

元素数量限制

对于数组、切片、字符串和map,Delve一次最多打印64个元素。想打印更多元素需要切片,如:

print arr
print arr[64:]

注意,在Delve中,map也是可以切片的,Delve会按照一个固定的顺序遍历map。

这个限制也可以通过config配置。

config max-string-len 100
config max-array-values 100

接口

接口打印格式如下:

接口名(具体类型) 值

例如:

(dlv) p iface1
interface {}(*struct main.astruct) *{A: 1, B: 2}
(dlv) p iface2
interface {}(*struct string) *"test"
(dlv) p err1
error(*struct main.astruct) *{A: 1, B: 2}

访问接口类型变量的字段有3种方式:

  1. 类型断言
(dlv) p iface1.(*main.astruct).B
2
  1. 使用.(data)
(dlv) p iface1.(data).B
2
  1. 如果是结构体类型或指向结构体的指针,也可省略类型断言。
(dlv) p iface1.B
2

包路径

当变量名相同时,需要加以包名区分。

p "some/package".A
p "some/other/package".A

Cgo的指针

Char指针被当作字符串,可以索引和切片。其他C指针做为Go切片,可以索引和切片。

CPU寄存器

CPU寄存器名称必须全大写,如RAX表示RAX寄存器。

如果本地变量名和寄存器重名,表达式的结果是本地变量,寄存器会被隐藏。

寄存器名前可以加任意多下划线,比如_RAX__RAX都表示RAX寄存器。

小于等于64比特的寄存器用uint64表示,大于64比特的寄存器用字符串以十六进制表示。

也可以将寄存器值表示为数组:

RegName.intN //表示为intN的数组
RegName.uintN //表示为uintN的数组
RegName.floatN //表示为floatN的数组

其中N必须是2的幂。

附录

  1. Delve:https://github.com/go-delve/delve
  2. Delve命令:https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md
  3. Gdlv:https://github.com/aarzilli/gdlv