開發(fā)程序過程中調(diào)試代碼是開發(fā)者經(jīng)常要做的一件事情,Go語言不像PHP、Python等動態(tài)語言,只要修改不需要編譯就可以直接輸出,而且可以動態(tài)的在運(yùn)行環(huán)境下打印數(shù)據(jù)。當(dāng)然Go語言也可以通過Println之類的打印數(shù)據(jù)來調(diào)試,但是每次都需要重新編譯,這是一件相當(dāng)麻煩的事情。我們知道在Python中有pdb/ipdb之類的工具調(diào)試,Javascript也有類似工具,這些工具都能夠動態(tài)的顯示變量信息,單步調(diào)試等。不過慶幸的是Go也有類似的工具支持:GDB。Go內(nèi)部已經(jīng)內(nèi)置支持了GDB,所以,我們可以通過GDB來進(jìn)行調(diào)試,那么本小節(jié)就來介紹一下如何通過GDB來調(diào)試Go程序。
GDB是FSF(自由軟件基金會)發(fā)布的一個強(qiáng)大的類UNIX系統(tǒng)下的程序調(diào)試工具。使用GDB可以做如下事情:
目前支持調(diào)試Go程序的GDB版本必須大于7.1。
編譯Go程序的時(shí)候需要注意以下幾點(diǎn)
GDB的一些常用命令如下所示
list
簡寫命令l
,用來顯示源代碼,默認(rèn)顯示十行代碼,后面可以帶上參數(shù)顯示的具體行,例如:list 15
,顯示十行代碼,其中第15行在顯示的十行里面的中間,如下所示。
10 time.Sleep(2 * time.Second)
11 c <- i
12 }
13 close(c)
14 }
15
16 func main() {
17 msg := "Starting main"
18 fmt.Println(msg)
19 bus := make(chan int)
break
簡寫命令 b
,用來設(shè)置斷點(diǎn),后面跟上參數(shù)設(shè)置斷點(diǎn)的行數(shù),例如b 10
在第十行設(shè)置斷點(diǎn)。
delete 簡寫命令 d
,用來刪除斷點(diǎn),后面跟上斷點(diǎn)設(shè)置的序號,這個序號可以通過info breakpoints
獲取相應(yīng)的設(shè)置的斷點(diǎn)序號,如下是顯示的設(shè)置斷點(diǎn)序號。
Num Type Disp Enb Address What
2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23
breakpoint already hit 1 time
backtrace
簡寫命令 bt
,用來打印執(zhí)行的代碼過程,如下所示:
#0 main.main () at /home/xiemengjun/gdb.go:23
#1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#3 0x0000000000000000 in ?? ()
info
info命令用來顯示信息,后面有幾種參數(shù),我們常用的有如下幾種:
info locals
顯示當(dāng)前執(zhí)行的程序中的變量值
info breakpoints
顯示當(dāng)前設(shè)置的斷點(diǎn)列表
info goroutines
顯示當(dāng)前執(zhí)行的goroutine列表,如下代碼所示,帶*的表示當(dāng)前執(zhí)行的
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
簡寫命令p
,用來打印變量或者其他信息,后面跟上需要打印的變量名,當(dāng)然還有一些很有用的函數(shù)$len()和$cap(),用來返回當(dāng)前string、slices或者maps的長度和容量。
whatis
用來顯示當(dāng)前變量的類型,后面跟上變量名,例如whatis msg
,顯示如下:
type = struct string
next
簡寫命令 n
,用來單步調(diào)試,跳到下一步,當(dāng)有斷點(diǎn)之后,可以輸入n
跳轉(zhuǎn)到下一步繼續(xù)執(zhí)行
coutinue
簡稱命令 c
,用來跳出當(dāng)前斷點(diǎn)處,后面可以跟參數(shù)N,跳過多少次斷點(diǎn)
set variable
該命令用來改變運(yùn)行過程中的變量值,格式如:set variable =
我們通過下面這個代碼來演示如何通過GDB來調(diào)試Go程序,下面是將要演示的代碼:
package main
import (
"fmt"
"time"
)
func counting(c chan<- int) {
for i := 0; i < 10; i++ {
time.Sleep(2 * time.Second)
c <- i
}
close(c)
}
func main() {
msg := "Starting main"
fmt.Println(msg)
bus := make(chan int)
msg = "starting a gofunc"
go counting(bus)
for count := range bus {
fmt.Println("count:", count)
}
}
編譯文件,生成可執(zhí)行文件gdbfile:
go build -gcflags "-N -l" gdbfile.go
通過gdb命令啟動調(diào)試:
gdb gdbfile
啟動之后首先看看這個程序是不是可以運(yùn)行起來,只要輸入run
命令回車后程序就開始運(yùn)行,程序正常的話可以看到程序輸出如下,和我們在命令行直接執(zhí)行程序輸出是一樣的:
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
[LWP 2771 exited]
[Inferior 1 (process 2771) exited normally]
好了,現(xiàn)在我們已經(jīng)知道怎么讓程序跑起來了,接下來開始給代碼設(shè)置斷點(diǎn):
(gdb) b 23
Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23.
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
[New LWP 3284]
[Switching to LWP 3284]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
上面例子b 23
表示在第23行設(shè)置了斷點(diǎn),之后輸入run
開始運(yùn)行程序?,F(xiàn)在程序在前面設(shè)置斷點(diǎn)的地方停住了,我們需要查看斷點(diǎn)相應(yīng)上下文的源碼,輸入list
就可以看到源碼顯示從當(dāng)前停止行的前五行開始:
(gdb) list
18 fmt.Println(msg)
19 bus := make(chan int)
20 msg = "starting a gofunc"
21 go counting(bus)
22 for count := range bus {
23 fmt.Println("count:", count)
24 }
25 }
現(xiàn)在GDB在運(yùn)行當(dāng)前的程序的環(huán)境中已經(jīng)保留了一些有用的調(diào)試信息,我們只需打印出相應(yīng)的變量,查看相應(yīng)變量的類型及值:
(gdb) info locals
count = 0
bus = 0xf840001a50
(gdb) p count
$1 = 0
(gdb) p bus
$2 = (chan int) 0xf840001a50
(gdb) whatis bus
type = chan int
接下來該讓程序繼續(xù)往下執(zhí)行,請繼續(xù)看下面的命令
(gdb) c
Continuing.
count: 0
[New LWP 3303]
[Switching to LWP 3303]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb) c
Continuing.
count: 1
[Switching to LWP 3302]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
每次輸入c
之后都會執(zhí)行一次代碼,又跳到下一次for循環(huán),繼續(xù)打印出來相應(yīng)的信息。
設(shè)想目前需要改變上下文相關(guān)變量的信息,跳過一些過程,并繼續(xù)執(zhí)行下一步,得出修改后想要的結(jié)果:
(gdb) info locals
count = 2
bus = 0xf840001a50
(gdb) set variable count=9
(gdb) info locals
count = 9
bus = 0xf840001a50
(gdb) c
Continuing.
count: 9
[Switching to LWP 3302]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
最后稍微思考一下,前面整個程序運(yùn)行的過程中到底創(chuàng)建了多少個goroutine,每個goroutine都在做什么:
(gdb) info goroutines
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
(gdb) goroutine 1 bt
#0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927
#1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:327
#2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:420
#3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22
#4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#6 0x0000000000000000 in ?? ()
通過查看goroutines的命令我們可以清楚地了解goruntine內(nèi)部是怎么執(zhí)行的,每個函數(shù)的調(diào)用順序已經(jīng)明明白白地顯示出來了。
本小節(jié)我們介紹了GDB調(diào)試Go程序的一些基本命令,包括run
、print
、info
、set variable
、coutinue
、list
、break
等經(jīng)常用到的調(diào)試命令,通過上面的例子演示,我相信讀者已經(jīng)對于通過GDB調(diào)試Go程序有了基本的理解,如果你想獲取更多的調(diào)試技巧請參考官方網(wǎng)站的GDB調(diào)試手冊,還有GDB官方網(wǎng)站的手冊。
更多建議: