Go 調(diào)試分析的高階技巧
大綱
Golang tools
nm
compile
objdump
pprof
trace
單元測(cè)試
執(zhí)行單元測(cè)試
統(tǒng)計(jì)代碼覆蓋率
程序 Debug
dlv 調(diào)試用法
gdb 調(diào)試
小技巧
不知道怎么斷點(diǎn)函數(shù)?
不知道調(diào)用上下文?
不知道怎么開(kāi)啟 pprof ?
為什么有時(shí)候單點(diǎn)調(diào)試的時(shí)候,總是非預(yù)期的執(zhí)行代碼?
總結(jié)
golang 高階調(diào)試
本文專(zhuān)注 golang debug 的一些技巧應(yīng)用,以及相關(guān)工具的實(shí)用用法,再也不用怕 golang 怎么調(diào)試。golang 作為一門(mén)現(xiàn)代化語(yǔ)音,出生的時(shí)候就自帶完整的 debug 手段:
golang tools 是直接集成在語(yǔ)言工具里,支持內(nèi)存分析,cpu分析,阻塞鎖分析等; delve,gdb 作為最常用的 debug 工具,讓你能夠更深入的進(jìn)入程序調(diào)試; delve 當(dāng)前是最友好的 golang 調(diào)試程序,ide 調(diào)試其實(shí)也是調(diào)用 dlv 而已,比如 goland; 單元測(cè)試的設(shè)計(jì)深入到語(yǔ)言設(shè)計(jì)級(jí)別,可以非常方便執(zhí)行單元測(cè)試并且生成代碼覆蓋率;
Golang tools
golang 從語(yǔ)言原生層面就集成了大量的實(shí)用工具,這些都是 Robert Griesemer, Rob Pike, Ken Thompson 這幾位大神經(jīng)驗(yàn)沉淀下的精華。你安裝好 golang 之后,執(zhí)行 go tool 就能看到內(nèi)置支持的所有工具了。
root@ubuntu:~# go tooladdr2lineasmbuildidcgocompilecoverdistdocfixlinknmobjdumppackpproftest2jsontracevet
我這里專(zhuān)注挑選幾個(gè) debug 常用的:
nm:查看符號(hào)表(等同于系統(tǒng) nm命令)objdump:反匯編工具,分析二進(jìn)制文件(等同于系統(tǒng) objdump命令)pprof:指標(biāo),性能分析工具 cover:生成代碼覆蓋率 trace:采樣一段時(shí)間,指標(biāo)跟蹤分析工具 compile:代碼匯編
nm
查看符號(hào)表的命令,等同于系統(tǒng)的 nm 命令,非常有用。在斷點(diǎn)的時(shí)候,如果你不知道斷點(diǎn)的函數(shù)符號(hào),那么用這個(gè)命令查一下就知道了(命令處理的是二進(jìn)制程序文件)。
# exmple 為你編譯的二進(jìn)制文件go tool nm ./example
第一列是地址,第二列是類(lèi)型,第三列是符號(hào):

compile
匯編某個(gè)文件
go tool compile -N -l -S example.go你就能看到你 golang 語(yǔ)言對(duì)應(yīng)的匯編代碼了(注意了,命令處理的是 golang 代碼文本),酷。
objdump
反匯編二進(jìn)制的工具,等同于系統(tǒng) objdump(注意了,命令解析的是二進(jìn)制格式的程序文件)。
go tool objdump example.ogo tool objdump -s DoFunc example.o? // 反匯編具體函數(shù)
匯編代碼這個(gè)東西在 90% 的場(chǎng)景可能都用不上,但是如果你處理過(guò) c 的程序,在某些特殊場(chǎng)景,通過(guò)反匯編一段邏輯來(lái)推斷應(yīng)用程序行為將是你唯一的出路。因?yàn)榫€上的代碼一般都是會(huì)開(kāi)啟編譯優(yōu)化,所以這里會(huì)導(dǎo)致你的代碼對(duì)不上。再者,線上不可能讓你隨意 attach 進(jìn)程,很多時(shí)候都是出 core 了,你就只有一個(gè) core 文件去排查。
pprof
pprof 支持四種類(lèi)型的分析:
CPU :CPU 分析,采樣消耗 cpu 的調(diào)用,這個(gè)一般用來(lái)定位排查程序里耗費(fèi)計(jì)算資源的地方; Memroy :內(nèi)存分析,一般用來(lái)排查內(nèi)存占用,內(nèi)存泄露等問(wèn)題; Block :阻塞分析,會(huì)采樣程序里阻塞的調(diào)用情況; Mutex :互斥鎖分析,采樣互斥鎖的競(jìng)爭(zhēng)情況;
我們這里詳細(xì)以?xún)?nèi)存占用分析舉例(其他的類(lèi)似),pprof 這個(gè)是內(nèi)存分析神器。基本上,golang 有了這個(gè)東西,99% 的內(nèi)存問(wèn)題(比如內(nèi)存泄露,內(nèi)存占用過(guò)大等等)都是可以非常快的定位出來(lái)的。首先,對(duì)于 golang 的內(nèi)存分析(或者其他的鎖消耗,cpu 消耗)我們明確幾個(gè)重要的點(diǎn):
golang 內(nèi)存 pprof 是采樣的,每 512KB 采樣一次; golang 的內(nèi)存采樣的是堆棧路徑,而不是類(lèi)型信息; golang 的內(nèi)存采樣入口一定是通過(guò) mProf_Malloc,mProf_Free這兩個(gè)函數(shù)。所以,如果是 cgo 分配的內(nèi)存,那么是沒(méi)有機(jī)會(huì)調(diào)用到這兩個(gè)函數(shù)的,所以如果是 cgo 導(dǎo)致的內(nèi)存問(wèn)題,go tool pprof 是分析不出來(lái)的;
詳細(xì)原理,可以復(fù)習(xí)另一篇文章:內(nèi)存分析;
分析的形式有兩種:
如果是 net/http/pporf方式開(kāi)啟的,那么可以直接在控制臺(tái)上輸入,瀏覽器就能看;另一種方式是先把信息 dump 到本地文件,然后用 go tool去分析(我們以這個(gè)舉例,因?yàn)檫@種方式才是生產(chǎn)環(huán)境通用的方式)
# 查看累計(jì)分配占用go tool pprof -alloc_space ./29075_20190523_154406_heap# 查看當(dāng)前的分配占用go tool pprof -inuse_space ./29075_20190523_154406_allocs
你也可以不指定類(lèi)型,直接 go tool pprof ./xxx ,進(jìn)入分析之后,調(diào)用 o 選項(xiàng),指定類(lèi)型:
我寫(xiě)了一個(gè) demo 程序,然后 dump 出了一份 heap 的 pprof 采樣文件,我們先通過(guò)這個(gè) pprof 得出一些結(jié)論,最后我再貼出源代碼,再品一品。
go tool pprof ./29075_20190523_154406_heap(pprof) o...sample_index = inuse_space //: [alloc_objects | alloc_space | inuse_objects | inuse_space]...(pprof) alloc_space(pprof) topShowing nodes accounting for 290MB, 100% of 290MB totalflat flat% sum% cum cum%140MB 48.28% 48.28% 140MB 48.28% main.funcA (inline)100MB 34.48% 82.76% 190MB 65.52% main.funcB (inline)50MB 17.24% 100% 140MB 48.28% main.funcC (inline)0 0% 100% 290MB 100% main.main0 0% 100% 290MB 100% runtime.main
這個(gè) top 信息表明了這么幾點(diǎn)信息:
main.funcA?這個(gè)函數(shù)現(xiàn)場(chǎng)分配了 140M 的內(nèi)存,main.funcB這個(gè)函數(shù)現(xiàn)場(chǎng)分配了 100M 內(nèi)存,main.funcC現(xiàn)場(chǎng)分配了 50M 內(nèi)存;現(xiàn)場(chǎng)的意思:純粹自己函數(shù)直接分配的,而不是調(diào)用別的函數(shù)分配的; 這些信息通過(guò) flat 得知; main.funcA?分配的 140M 內(nèi)存純粹是自己分配的,沒(méi)有調(diào)用別的函數(shù)分配過(guò)內(nèi)存;這個(gè)信息通過(guò) main.funcAflat 和 cum 都為 140 M 得出;main.funcB?自己分配了 100MB,并且還調(diào)用了別的函數(shù),別的函數(shù)里面涉及了 90M 的內(nèi)存分配;這個(gè)信息通過(guò) main.funcBflat 和 cum 分別為 100 M,190M 得出;main.funcC?自己分配了 50MB,并且還調(diào)用了別的函數(shù),別的函數(shù)里面涉及了 90M 的內(nèi)存分配;這個(gè)信息通過(guò) main.funcCflat 和 cum 分別為 50 M,140 M 得出;main.main:所有分配內(nèi)存的函數(shù)調(diào)用都是走這個(gè)函數(shù)出去的。main 函數(shù)本身沒(méi)有函數(shù)分配,但是他調(diào)用的函數(shù)分配了 290M;
demo 的源代碼:
package mainimport ("net/http"_ "net/http/pprof")func funcA() []byte {a := make([]byte, 10*1024*1024)return a}func funcB() ([]byte, []byte) {a := make([]byte, 10*1024*1024)b := funcA()return a, b}func funcC() ([]byte, []byte, []byte) {a := make([]byte, 10*1024*1024)b, c := funcB()return a, b, c}func main() {for i := 0; i < 5; i++ {funcA()funcB()funcC()}http.ListenAndServe("0.0.0.0:9999", nil)}
dump 命令:
curl -sS 'http://127.0.0.1:9999/debug/pprof/heap?seconds=5' -o heap.pporf對(duì)照著代碼,再品一品。采樣原理之前已經(jīng)詳細(xì)分析過(guò)了,flat,cum 這兩個(gè)字段的含義?這個(gè)可以復(fù)習(xí)下:golang 內(nèi)存管理分析。
trace
程序 trace 調(diào)試
go tool trace -http=":6060" ./ssd_336959_20190704_105540_tracetrace 這個(gè)命令允許你跟蹤采集一段時(shí)間的信息,然后 dump 成文件,最后調(diào)用 go tool trace 分析 dump 文件,并且以 web 的形式打開(kāi)。
單元測(cè)試
單元測(cè)試的重要性就不再論述。golang 里面 _test.go 結(jié)尾的文件認(rèn)為是測(cè)試文件,golang 作為現(xiàn)代化的語(yǔ)言,語(yǔ)言工具層面支持單元測(cè)試。
執(zhí)行單元測(cè)試
執(zhí)行單元測(cè)試有兩種方式:
go test 直接運(yùn)行,這個(gè)是最簡(jiǎn)單的; 先編譯測(cè)試文件,再運(yùn)行。這種方式更靈活;
go test 運(yùn)行
// 直接在你項(xiàng)目目錄里運(yùn)行 go test .go test .// 指定運(yùn)行函數(shù)go test -run=TestPutAndGetKeyValue// 打印詳細(xì)信息go test -v
編譯,運(yùn)行
本質(zhì)上,golang 跑單測(cè)是先編譯 *_test.go 文件,編譯成二進(jìn)制后,再運(yùn)行這個(gè)二進(jìn)制文件。你執(zhí)行 go test 的時(shí)候,工具幫你做好了,這些動(dòng)作其實(shí)也是可以拆開(kāi)來(lái)自己做的。
編譯生成單元測(cè)試可執(zhí)行文件:
// 先編譯出 .test 文件$ go test -c?
// 指定跑某一個(gè)文件$ ./raftexample.test -test.timeout=10m0s -test.v=true -test.run=TestPutAndGetKeyValue
這種方式通常會(huì)出現(xiàn)在以下幾種場(chǎng)景:
這臺(tái)機(jī)器上編譯,另一個(gè)地方跑單測(cè); debug 單測(cè)程序;
統(tǒng)計(jì)代碼覆蓋率
golang 的代碼覆蓋率是基于單測(cè)的,由單測(cè)作為出發(fā)點(diǎn),來(lái)看你的業(yè)務(wù)代碼覆蓋率。
操作很簡(jiǎn)單:
加一個(gè) -coverprofile的參數(shù),聲明在跑單測(cè)的時(shí)候,記錄代碼覆蓋率;使用 go tool cover命令分析,得出覆蓋率報(bào)告;
go test -coverprofile=coverage.outgo tool cover -func=coverage.out
類(lèi)似如下:
root@ubuntu:~/opensource/readcode-etcd-master/src/go.etcd.io/etcd/contrib/raftexample# go tool cover -func=coverage.outgo.etcd.io/etcd/v3/contrib/raftexample/httpapi.go:33: ServeHTTP 25.0%go.etcd.io/etcd/v3/contrib/raftexample/httpapi.go:108: serveHttpKVAPI 0.0%go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:41: newKVStore 100.0%go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:50: Lookup 100.0%go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:57: Propose 75.0%go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:71: readCommits 55.0%go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:107: getSnapshot 100.0%go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:113: recoverFromSnapshot 85.7%go.etcd.io/etcd/v3/contrib/raftexample/listener.go:30: newStoppableListener 75.0%go.etcd.io/etcd/v3/contrib/raftexample/listener.go:38: Accept 92.9%go.etcd.io/etcd/v3/contrib/raftexample/main.go:24: main 0.0%total: (statements) 57.1%
這樣的話,你就知道每個(gè)函數(shù)的代碼覆蓋率。實(shí)踐證明,無(wú)數(shù)的程序 bug 都是出現(xiàn)在平時(shí)沒(méi)有覆蓋到的程序邏輯上。所以,保證你的代碼都被測(cè)試覆蓋過(guò)是保證項(xiàng)目質(zhì)量的有效手段,golang 的 go test 幫你做到這一點(diǎn)。
程序 Debug
程序的調(diào)試主要由兩個(gè)工具:
dlv gdb
這里推薦 dlv,因?yàn)?gdb 功能實(shí)在是有限,gdb 不理解 golang 的業(yè)務(wù)類(lèi)型和協(xié)程。但是 gdb 有一個(gè)功能是無(wú)法替代的,就是 gcore 的功能。
dlv 調(diào)試用法
調(diào)試二進(jìn)制
dlv exec [flags] 舉例:
dlv exec ./exampledlv?調(diào)試二進(jìn)制,并帶參數(shù)
dlv exec ./example -- --audit=./d調(diào)試進(jìn)程
dlv attach ${pid} [executable] [flags]進(jìn)程號(hào)是必選的。
舉例:
dlv attach 12808 ./example調(diào)試 core 文件
dlv 調(diào)試core文件;并且標(biāo)準(zhǔn)輸出導(dǎo)出到文件
dlv core [flags] dlv core ./example core.277282調(diào)試常用語(yǔ)法
系統(tǒng)整理
程序運(yùn)行
call :call 函數(shù)(注意了,這個(gè)會(huì)導(dǎo)致整個(gè)程序運(yùn)行的) continue :往下運(yùn)行 next :?jiǎn)尾秸{(diào)試 restart :重啟 step :?jiǎn)尾秸{(diào)試,某個(gè)函數(shù) step-instruction :?jiǎn)尾秸{(diào)試某個(gè)匯編指令 stepout :從當(dāng)前函數(shù)跳出
斷點(diǎn)相關(guān)
break (alias: b) :設(shè)置斷點(diǎn) breakpoints (alias: bp) ?:打印所有的斷點(diǎn)信息 clear :清理斷點(diǎn) clearall :清理所有的斷點(diǎn) condition (alias: cond) ?:設(shè)置條件斷點(diǎn) on :設(shè)置一段命令,當(dāng)斷點(diǎn)命中的時(shí)候 trace (alias: t) :設(shè)置一個(gè)跟蹤點(diǎn),這個(gè)跟蹤點(diǎn)也是一個(gè)斷點(diǎn),只不過(guò)運(yùn)行到的時(shí)候不會(huì)斷住程序,只是打印一行信息,這個(gè)命令在某些場(chǎng)景是很有用的,比如你斷住程序就會(huì)影響邏輯(業(yè)務(wù)有超時(shí)),而你僅僅是想打印某個(gè)變量而已,那么用這種類(lèi)型的斷點(diǎn)就行;;
信息打印
args : 打印程序的傳參 examinemem (alias: x) ?:這個(gè)是神器,解析內(nèi)存用的,和 gdb 的 x 命令一樣; locals :打印本地變量? print (alias: p) :打印一個(gè)表達(dá)式,或者變量? regs :打印寄存器的信息? set :set 賦值? vars :打印全局變量(包變量)? whatis :打印類(lèi)型信息
協(xié)程相關(guān)
goroutine (alias: gr) :打印某個(gè)特定協(xié)程的信息?
goroutines (alias: grs) ?:列舉所有的協(xié)程?
thread (alias: tr) :切換到某個(gè)線程?
threads :打印所有的線程信息
棧相關(guān)
deferred :在 defer 函數(shù)上下文里執(zhí)行命令?
down :上堆棧?
frame :跳到某個(gè)具體的堆棧?
stack (alias: bt) ?:打印堆棧信息?
up :下堆棧
其他命令
config :配置變更? disassemble (alias: disass) :反匯編? edit (alias: ed) :略? exit (alias: quit | q) :略? funcs :打印所有函數(shù)符號(hào)? libraries :打印所有加載的動(dòng)態(tài)庫(kù)? list (alias: ls | l) :顯示源碼? source :加載命令? sources :打印源碼? types :打印所有類(lèi)型信息
以上就是完整的 dlv 的支持的命令,從這個(gè)來(lái)看,是完全滿(mǎn)足我們的調(diào)試需求的(有的只適用于開(kāi)發(fā)調(diào)試環(huán)節(jié),比如線上的程序不可能讓你隨意單步調(diào)試的,有的使用于線上生產(chǎn)環(huán)節(jié))。
應(yīng)用舉例
打印全局變量
(dlv) vars這個(gè)非常有用,幫助你看一些全局變量。
條件斷點(diǎn)
# 先斷點(diǎn)(dlv) b?# 查看斷點(diǎn)信息(dlv) bp# 然后定制條件(dlv) condition 2 i==2 && j==7 && z==32
查看堆棧
# 展示所有堆棧(dlv) goroutines# 所有堆棧展開(kāi)(dlv) goroutines -t
解析內(nèi)存
(dlv) x -fmt hex -len 20 0xc00008af38x 命令和 gdb 的 x 是一樣的。
gdb 調(diào)試
gdb 對(duì) golang 的調(diào)試支持是通過(guò)一個(gè) python 腳本文件 src/runtime/runtime-gdb.py 來(lái)擴(kuò)展的,所以功能非常有限。gdb 只能做到最基本的變量打印,卻理解不了 golang 的一些特殊類(lèi)型,比如 channel,map,slice 等,gdb 原生是無(wú)法調(diào)適 goroutine 協(xié)程的,因?yàn)檫@個(gè)是用戶(hù)態(tài)的調(diào)度單位,gdb 只能理解線程。所以只能通過(guò) python 腳本的擴(kuò)展,把協(xié)程結(jié)構(gòu)按照鏈表輸出出來(lái),支持的命令:

gdb當(dāng)前只支持6個(gè)命令:
3個(gè) cmd 命令
info goroutines;打印所有的goroutines goroutine ${id} bt;打印一個(gè)goroutine的堆棧 iface;打印靜態(tài)或者動(dòng)態(tài)的接口類(lèi)型
3個(gè)函數(shù)
len;打印string,slices,map,channels 這四種類(lèi)型的長(zhǎng)度 cap;打印slices,channels 這兩種類(lèi)型的cap dtype;強(qiáng)制轉(zhuǎn)換接口到動(dòng)態(tài)類(lèi)型。
打印全局變量 (注意單引號(hào))
(gdb) p 'runtime.firstmoduledata'由于 gdb 不理解 golang 的一些類(lèi)型系統(tǒng),所以調(diào)試打印的時(shí)候經(jīng)常打印不出來(lái),這個(gè)要注意下。
打印數(shù)組變量長(zhǎng)度
(gdb) p $len(xxx)所以,我一般只用 gdb 來(lái) gcore 而已。
小技巧
不知道怎么斷點(diǎn)函數(shù)?
有時(shí)候不知道怎么斷點(diǎn)函數(shù):可以通過(guò)nm查詢(xún)下,然后再斷點(diǎn),就一定能斷到了。


不知道調(diào)用上下文?
在你的代碼里添加一行:
debug.PrintStack()這樣就能當(dāng)前代碼位置的堆棧給打印出來(lái),這樣你就直到怎么函數(shù)的調(diào)用路徑了。
不知道怎么開(kāi)啟 pprof ?
pprof 功能有兩種開(kāi)啟方式,對(duì)應(yīng)兩種包:
net/http/pprof :使用在 web 服務(wù)器的場(chǎng)景; runtime/pprof ?:使用在非服務(wù)器應(yīng)用程序的場(chǎng)景;
這兩個(gè)本質(zhì)上是一致的,net/http/pporf 也只是在 runtime/pprof 上的一層 web 封裝。
net/http/pprof 方式
import _ "net/http/pprof"runtime/pprof 方式
這種通常用于程序調(diào)優(yōu)的場(chǎng)景,程序只是一個(gè)應(yīng)用程序,跑一次就結(jié)束,你想找到瓶頸點(diǎn),那么通常會(huì)使用到這個(gè)方式。
// cpu pprof 文件路徑f, err := os.Create("cpufile.pprof")if err != nil {log.Fatal(err)}// 開(kāi)啟 cpu pprofpprof.StartCPUProfile(f)defer pprof.StopCPUProfile()
為什么有時(shí)候單點(diǎn)調(diào)試的時(shí)候,總是非預(yù)期的執(zhí)行代碼?
這種情況一般是被編譯器優(yōu)化了,比如函數(shù)內(nèi)聯(lián)了,編譯出的二進(jìn)制刪減了無(wú)效邏輯、無(wú)效參數(shù)。這種情況就會(huì)導(dǎo)致你 dlv 單步調(diào)試的時(shí)候,總是非預(yù)期的執(zhí)行,或者打印某些變量打印不出來(lái)。這種情況解決方法就是:禁止編譯優(yōu)化。
go build -gcflags "-N -l"總結(jié)
該篇文章系統(tǒng)的分享了 golang 程序調(diào)試的技巧和用法:
語(yǔ)言工具包里內(nèi)置 tool 工具,支持匯編,反匯編,pprof 分析,符號(hào)表查詢(xún)等實(shí)用功能; 語(yǔ)言工具包集成單元測(cè)試,代碼覆蓋率依賴(lài)于單元測(cè)試的觸發(fā); 常用 dlv/gdb 這兩個(gè)工具作為大殺器,可以分析二進(jìn)制,進(jìn)程,core 文件; 有疑問(wèn)可以私信交流(我好像沒(méi)有留言功能),之后會(huì)有個(gè)專(zhuān)輯分享一些線上問(wèn)題排查的實(shí)際案例;
推薦閱讀
站長(zhǎng) polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語(yǔ)言中文網(wǎng)
每天為你
分享 Go 知識(shí)
Go愛(ài)好者值得關(guān)注
