dlv git 仓库: https://github.com/go-delve/delve/tree/master/Documentation/installation
Delve目的就是为了解决开发者在使用 GDB 调试中遇到的各种各样的问题。我们开始详细的介绍一些使用Delve 调试代码的例子。
安装
首先默认你已经安装了 Go 环境,安装命令很简单,一句话。
go install github.com/go-delve/delve/cmd/dlv
注意:如果你使用Go1.5,你必须在运行这个命令前设置GO15VENDOREXPERIMENT=1
调试代码
首先得说明一下,实诚点说,当你想用debug 工具的时候,你的代码估计已经不按照你想象的方式运行了。只是你不知道为什么会这样,因此你可能需要换一种方式启动你的程序,下面我们来演示一下如果使用dlv
来启动你的程序。我们的示例代码如下:
package main
import (
"fmt"
"sync"
"time"
)
func dostuff(wg *sync.WaitGroup, i int) {
fmt.Printf("goroutine id %d\n", i)
time.Sleep(300 * time.Second)
fmt.Printf("goroutine id %d\n", i)
wg.Done()
}
func main() {
var wg sync.WaitGroup
workers := 10
wg.Add(workers)
for i := 0; i < workers; i++ {
go dostuff(&wg, i)
}
wg.Wait()
}
在这个示例代码中,我们创建了10个goroutine
,这种代码对于 GDB 来说是几乎不可读的,因为它对于goroutine
的支持很差。但是Delve
作为一个专业的 Go 调试器,对于goroutine
这种杀手级功能还是非常了解的。下面我们来启动程序。
dlv debug test-debug.go
运行这个命令,dlv会去编译你的代码,然后传一些参数给编译器,好让编译器编译出来更加方便调试的可执行文件,然后启动了你的程序,并且attach
上去,这样你的console就会停留在了debug session
,下面就可以调试程序了。
首先我们在main函数上设置一个断点。
(dlv) break main.main
Breakpoint 1 set at 0x22d3 for main.main() ./test-debug.go:16
输出信息里面告诉了我们断点的 ID和断点的位置,函数名和文件名以及所在行数。我们使用continue
命令让程序运行到我们打断点的地方。
(dlv) continue
> main.main() ./test-debug.go:16 (hits goroutine(1):1 total:1) (PC: 0x22d3)
11: time.Sleep(300 * time.Second)
12: fmt.Printf("goroutine id %d\n", i)
13: wg.Done()
14: }
15:
=> 16: func main() {
17: var wg sync.WaitGroup
18: workers := 10
19:
20: wg.Add(workers)
21: for i := 0; i < workers; i++ {
(dlv)
现在程序就停在了第一个断点,现在我们可以使用next
命令让程序运行到下一句话,如果你想继续向下,可以直接按回车(Delve会重复上一条命令如果你按下回车键)。
(dlv) next
> main.main() ./test-debug.go:17 (PC: 0x22d7)
12: fmt.Printf("goroutine id %d\n", i)
13: wg.Done()
14: }
15:
16: func main() {
=> 17: var wg sync.WaitGroup
18: workers := 10
19:
20: wg.Add(workers)
21: for i := 0; i < workers; i++ {
22: go dostuff(&wg, i)
(dlv)
> main.main() ./test-debug.go:18 (PC: 0x22f1)
13: wg.Done()
14: }
15:
16: func main() {
17: var wg sync.WaitGroup
=> 18: workers := 10
19:
20: wg.Add(workers)
21: for i := 0; i < workers; i++ {
22: go dostuff(&wg, i)
23: }
(dlv)
> main.main() ./test-debug.go:20 (PC: 0x22fa)
15:
16: func main() {
17: var wg sync.WaitGroup
18: workers := 10
19:
=> 20: wg.Add(workers)
21: for i := 0; i < workers; i++ {
22: go dostuff(&wg, i)
23: }
24: wg.Wait()
25: }
(dlv)
现在我们可以尝试使用print
命令去看一下变量的值。
(dlv) print wg
sync.WaitGroup {
state1: [12]uint8 [0,0,0,0,0,0,0,0,0,0,0,0],
sema: 0,}
(dlv) print workers
10
(dlv)
同时你也可以输出一个表达式
(dlv) print workers < 100
true
下面我们在另外一个函数dostuff
上设置一个断点
(dlv) break dostuff
Breakpoint 2 set at 0x2058 for main.dostuff() ./test-debug.go:9
我们使用continue
到我们设置断点的地方,然后next
(dlv) next
goroutine id 3
> main.dostuff() ./test-debug.go:10 (PC: 0x205f)
5: "sync"
6: "time"
7: )
8:
9: func dostuff(wg *sync.WaitGroup, i int) {
=> 10: fmt.Printf("goroutine id %d\n", i)
11: time.Sleep(300 * time.Second)
12: fmt.Printf("goroutine id %d\n", i)
13: wg.Done()
14: }
15:
(dlv)
可以看到Delve会告诉你目前的goroutine id,我们试试输出一下i和wg.
(dlv) print i
4
(dlv) print wg
*sync.WaitGroup {
state1: [12]uint8 [1,0,0,0,10,0,0,0,0,0,0,0],
sema: 0,}
我们创建了10个goroutine,如果你继续使用next,你会发现你还是在同一个goroutine下。这样就避免了被调试器跳转到了另外的goroutine下导致不必要的调试错误。可见还是为 Go 而生的调试器才是真爱啊。
进阶调试
其实很多时候,我们调试的代码可能是daemon
程序或者需要实现编译好在不同机器运行的程序。这就需要我们attach
到一个已经在运行中的程序上,下面我们就使用上面的代码来演示一下如何attach
到一个程序上进行调试。首先将刚才的程序运行起来,我这里直接使用了
go build test-debug.go
./test-debug
然后使用ps查看正在运行的程序pid
501 40994 549 0 12:08AM ttys003 0:00.00 ./test-debug
然后我们attach
上去
dlv attach 40994
可以看到,熟悉的debug seesion
又回来了。下面我们可以继续使用上面的命令去设置断点了
(dlv) break dostuff
Breakpoint 1 set at 0x2058 for main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:9
(dlv) break dostuff:3
Breakpoint 2 set at 0x2144 for main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12
我使用continue
使程序运行到我设置断点的地方
(dlv) continue
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(18):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(19):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(26):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(23):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(24):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(20):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(21):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(25):1 total:8) (PC: 0x2144)
7: )
8:
9: func dostuff(wg *sync.WaitGroup, i int) {
10: fmt.Printf("goroutine id %d\n", i)
11: time.Sleep(300 * time.Second)
=> 12: fmt.Printf("goroutine id %d\n", i)
13: wg.Done()
14: }
15:
16: func main() {
17: var wg sync.WaitGroup
(dlv)
可以看到,Delve已经打印出来了当前正在运行的goroutine,下面我们print
一下我们当前的i
(dlv) print i
7
(dlv) print wg
*sync.WaitGroup {
state1: [12]uint8 [1,0,0,0,10,0,0,0,0,0,0,0],
sema: 0,}
和上面一样,而且attach
到这个进程后,也可以把对应的源码显示出来,是不是很强大呢。更多的功能就自己参考文档摸索吧。