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
选项。
同调试源代码基本一致,只是将debug
换成了test
。
$ gdlv test
也可通过-d
选项指定代码目录。
打开调试窗口以后默认断点在main函数处,在Command窗口底部可以输入Delve命令。
Gdlv启动调试默认会停在main
函数处,但是Delve并不会停在main
处,所以在直接用dlv debug
命令调试时,需要先在main
函数处设置断点,然后运行至断点处,具体命令见后面章节。
在Gdlv的Listing窗口,右键会弹出一个菜单,一个是设置断点,一个是运行到指定行。
右键断点,可以禁用/启动断点,编辑断点,以及运行到断点处。
编辑断点可以设置断点条件。
在Command窗口的顶部有运行、单步、跳入跳出按钮。
右上角的NEW WINDOW
下拉框可以打开更多窗口,查看其他信息。比如选则Breakpoints
可以查看所有断点。
Variables窗口可以查看本地变量,通过Filter可以过滤变量。Full Types单选框可以显示完整类型信息,Address单选框可以显示变量地址。
接下来要介绍的是Devle的调试命令,我们可以在Gdlv的Command窗口输入这些命令。
设置断点的命令是break
,break
的别名是b
,因此也可以使用b
。
此外断点也可以命名,格式为break 断点名
,或者b 断点名
。
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
。
当我们通过break
命令设置断点时,会打印出断点ID。
如果设置断点时指定了名称,那么也可以通过断点名删除。
通过这个命令删除断点不会在Gdlv中实时显示,需要做一个别的操作刷新页面,删除的断点才会消失。
批量清除
命令为clearall [
,如果省略
则清除所有断点,否则,删除与
匹配处的断点。
这个命令在Gdlv中也不可用。
设置断点条件的命令是condition
,别名是cond
。这个命令在Gdlv中也不可用,所以在Gdlv中只能通过UI来设置断点条件。条件是一个布尔表达式,关于Delve表达式见表达式。
满足条件进入断点
命令格式如下:
cond 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
进入断点时执行命令。格式为:
on
支持的
有以下5个:
print
打印表达式stack
打印调用栈goroutine
显示goroutinetrace
将断点变成跟踪点(trace point)cond
等同于cond
修改命令使用-edit
选项。
on -edit
禁用/启用断点可以通过Gdlv界面进行操作。Delve官方文档给了一个toggle
命令来禁用/启用断点,但是实测会提示命令不可用,不知道是不是版本的原因。
next [n]
Delve中,这个next
命令可以指定执行几行代码。但在Gdlv中,n
会被忽略,始终执行1行。
命令为step
,别名为s
。
命令为stepout
,别名为so
。
在Gdlv中不支持so
。
命令为step-instruction
,别名为si
。CPU指令级别的单步。
命令restart
,别名r
。
命令:exit
。
命令为continue
,别名为c
。
默认执行到下一个断点或至程序结束。也可以指定location,运行到指定位置。
c
在Gdlv中,
会被忽略。
命令格式:
[goroutine ] [frame ] args [-v] []
打印函数参数,-v
选项用于显示参数详细信息,
可以过滤变量。
args
args a
goroutine 1 args
args
在Gdlv中不可用,当进入函数后,参数和本地变量会自动显示在Valriables窗口。
locals
的使用与args
一样。
[goroutine ] [frame ] locals [-v] []
locals
在Gdlv中也不可用,原因同args
。
vars [-v] []
只打印包级变量。
添加监控表达式
dispaly -a
将
添加到监控列表,每当程序停下时都会打印出监控列表中表达式的值。
(dlv) display -a a+1
0: a+1 = 2
:
前面的0
是表达式的下标,删除时会用到。
查看监控表达式列表
display
不带任何参数会打印出所有被监控的表达式以及表达式的值。
(dlv) display
0: a+1 = 3
1: b = 4
这里也会打印出表达式的编号。
删除表达式监控
display -d
这里的
是表达式的编号。
命令为print
,别名p
。命令格式:
[goroutine ] [frame ] print
与display
不同的是,print
只会打印表达式的值1次。
print a
goroutine 1 print b
regs [-a]
打印CPU寄存器的值,-a
选项打印更多寄存器的值。
命令examinemem
,别名x
。
x [-fmt ] [-count|-len ] [-size ]
从开始按
格式以
字节为单位打印
次。
有5种:
表示次数,默认1,最大值1000。
表示字节数,默认1,最大值8。
表示内存地址。
示例:
(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
打印表达式类型。
[goroutine ] [frame ] set =
只能修改数字和指针类型的值。
查看所有命令文档。
help
查看某个命令文档。
help xxx
Delve支持的表达式语法是Go语法的子集。
<-
、++
、--
外的单目和双目运算符string
、[]byte
和[]rune
相互转换a.A
)map
cap
、len
、complex
、imag
、real
。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
的第一个元素,有两种方式:
print c1.sa[0]
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种方式:
(dlv) p iface1.(*main.astruct).B
2
.(data)
(dlv) p iface1.(data).B
2
(dlv) p iface1.B
2
当变量名相同时,需要加以包名区分。
p "some/package".A
p "some/other/package".A
Char指针被当作字符串,可以索引和切片。其他C指针做为Go切片,可以索引和切片。
CPU寄存器名称必须全大写,如RAX
表示RAX
寄存器。
如果本地变量名和寄存器重名,表达式的结果是本地变量,寄存器会被隐藏。
寄存器名前可以加任意多下划线,比如_RAX
、__RAX
都表示RAX
寄存器。
小于等于64比特的寄存器用uint64表示,大于64比特的寄存器用字符串以十六进制表示。
也可以将寄存器值表示为数组:
RegName.intN //表示为intN的数组
RegName.uintN //表示为uintN的数组
RegName.floatN //表示为floatN的数组
其中N必须是2的幂。