一文詳解如何在調(diào)試過程中查找 Goroutine
Goroutines 是大多數(shù)用 Go 編寫的程序的重要組成部分。但是,使用大量 goroutines 會(huì)使程序難以調(diào)試。那怎么辦?在此博文中,我們將介紹如何使用自定義數(shù)據(jù)為 goroutine 加上標(biāo)簽。
目錄
在 IDE 下使用 在命令行下使用 性能影響 使用自定義庫啟用調(diào)試標(biāo)簽
讓我們以向 Web 服務(wù)器發(fā)出請(qǐng)求的應(yīng)用程序?yàn)槔?/p>
package?main
?
import?(
????"io"
????"io/ioutil"
????"math/rand"
????"net/http"
????"strconv"
????"strings"
????"time"
)
?
func?fakeTraffic()?{
????//?Wait?for?the?server?to?start
????time.Sleep(1?*?time.Second)
?
????pages?:=?[]string{"/",?"/login",?"/logout",?"/products",?"/product/{productID}",?"/basket",?"/about"}
?
????activeConns?:=?make(chan?struct{},?10)
?
????c?:=?&http.Client{
????????Timeout:?10?*?time.Second,
????}
?
????i?:=?int64(0)
?
????for?{
????????activeConns?<-?struct{}{}
????????i++
?
????????page?:=?pages[rand.Intn(len(pages))]
?
????????//?We?need?to?launch?this?using?a?closure?function?to
????????//?ensure?that?we?capture?the?correct?value?for?the
????????//?two?parameters?we?need:?page?and?i
????????go?func(p?string,?rid?int64)?{
????????????makeRequest(activeConns,?c,?p,?rid)
????????}(page,?i)
????}
}
?
func?makeRequest(done?chan?struct{},?c?*http.Client,?page?string,?i?int64)?{
????defer?func()?{
????????//?Unblock?the?next?request?from?the?queue
????????<-done
????}()
?
????page?=?strings.Replace(page,?"{productID}",?"abc-"+strconv.Itoa(int(i)),?-1)
????r,?err?:=?http.NewRequest(http.MethodGet,?"http://localhost:8080"+page,?nil)
????if?err?!=?nil?{
????????return
????}
?
????resp,?err?:=?c.Do(r)
????if?err?!=?nil?{
????????return
????}
????defer?resp.Body.Close()
?
????_,?_?=?io.Copy(ioutil.Discard,?resp.Body)
?
????time.Sleep(time.Duration(10+rand.Intn(40))?+?time.Millisecond)
}
在 IDE 下使用
如果我們?cè)谡{(diào)試器(debugger)中分析此代碼,我們?nèi)绾沃?makeRequest goroutines 在做什么?當(dāng)我們看這樣的清單時(shí),這些 goroutine 的執(zhí)行上下文什么?

這就是 GoLand 新版本支持讀取 goroutines 標(biāo)簽的緣由。
我們調(diào)整下上面的代碼:(polaris 注:pprof 是標(biāo)準(zhǔn)庫的 runtime/pprof )
go?func(p?string,?rid?int64)?{
????labels?:=?pprof.Labels("request",?"automated",?"page",?p,?"rid",?strconv.Itoa(int(rid)))
????pprof.Do(context.Background(),?labels,?func(_?context.Context)?{
????????makeRequest(activeConns,?c,?p,?rid)
????})
}(page,?i)
現(xiàn)在,當(dāng)在調(diào)試器中運(yùn)行相同的代碼時(shí),我們將看到以下視圖:

看起來好多了。現(xiàn)在,我們可以看到在標(biāo)簽中設(shè)置的所有信息。而且,最重要的是,我們還可以看到通過函數(shù)調(diào)用在后臺(tái)啟動(dòng)的其他 goroutine,它們都會(huì)自動(dòng)攜帶標(biāo)簽。
由于 HTTP HandleFunc 這種形式的處理程序非常受歡迎,并且可以與其他處理程序類型進(jìn)行比較,因此讓我們看一下如何調(diào)整下面的代碼以設(shè)置標(biāo)簽。
我們的原始代碼將 m 用作 *http.ServeMux(或 *github.com/gorilla/mux.Router),看起來像這樣:m.HandleFunc("/", homeHandler)。
應(yīng)用標(biāo)簽代碼后,它將變?yōu)槿缦滤荆?/p>
m.HandleFunc("/",?func(w?http.ResponseWriter,?r?*http.Request)?{
????labels?:=?pprof.Labels("path",?r.RequestURI,?"request",?"real")
????pprof.Do(context.Background(),?labels,?func(_?context.Context)?{
????????homeHandler(w,?r)
????})
})
這將標(biāo)記處理每個(gè) HTTP 請(qǐng)求的 goroutine,如下所示。

由于可以訪問請(qǐng)求對(duì)象,因此可以使用比示例代碼中更復(fù)雜的數(shù)據(jù)填充標(biāo)簽。
在命令行下使用
如果直接在命令行中使用 Delve,則需要使用 1867862[1] 或更高版本的 Delve。這些更改將包含在下一個(gè)版本中,而當(dāng)前v1.4.0 版本中未包含。
要查看標(biāo)簽,請(qǐng)?jiān)谡{(diào)試會(huì)話期間調(diào)用 goroutines -l 命令,以查看到與 IDE 中相同的數(shù)據(jù)。

性能影響
隨之而來的自然問題是:使用上述代碼對(duì)性能會(huì)有影響嗎?
答案是肯定的,設(shè)置這些標(biāo)簽確實(shí)會(huì)降低性能。通常,它的影響很小,但是仍然會(huì)存在,因此最好使用一些基準(zhǔn)測(cè)試代碼在自己的硬件上進(jìn)行測(cè)試。
考慮到這種影響,就會(huì)出現(xiàn)下一個(gè)問題:如果涉及性能,則意味著每次需要進(jìn)行調(diào)試時(shí),我都需要應(yīng)用和撤消代碼。這會(huì)影響我的開發(fā)速度,這能做得更好嗎?
使用自定義庫啟用調(diào)試標(biāo)簽
要回答上述問題并允許我們的調(diào)試代碼在不影響性能的情況下進(jìn)行編譯,請(qǐng)使用 github.com/dlsniper/debugger[2] 庫并更改我們的 makeRequest 代碼以包括以下函數(shù)調(diào)用:
func?makeRequest(done?chan?struct{},?c?*http.Client,?page?string,?i?int64)?{
????defer?func()?{
????????//?Unblock?the?next?request?from?the?queue
????????<-done
????}()
?
????debugger.SetLabels(func()?[]string?{
????????return?[]string{
????????????"request",?"automated",
????????????"page",?page,
????????????"rid",?strconv.Itoa(int(i)),
????????}
????})
?//?..
}
在調(diào)試器中運(yùn)行此代碼之前,我們需要進(jìn)行其他更改。我們需要在運(yùn)行配置的 Go 工具參數(shù)字段中添加 -tags debugger。否則,該庫將加載生產(chǎn)代碼,標(biāo)簽將不起作用。

此處顯示的庫支持標(biāo)準(zhǔn)的 http.HandlerFunc 簽名,以方便在現(xiàn)有應(yīng)用程序中使用。
回到我們的代碼,如下所示:m.HandleFunc("/", homeHandler)。
要將標(biāo)簽添加到這些處理程序,我們可以將代碼更改為如下所示:
m.HandleFunc("/",?debugger.Middleware(homeHandler,?func(r?*http.Request)?[]string?{
????return?[]string{
????????"request",?"real",
????????"path",?r.RequestURI,
????}
}))
專業(yè)提示:
在單個(gè)函數(shù)或方法中對(duì) debugger.SetLabels[3] 函數(shù)進(jìn)行多次調(diào)用,可以更輕松地跟蹤執(zhí)行進(jìn)度并過濾掉不需要的數(shù)據(jù)。
專業(yè)提示:
可以復(fù)制運(yùn)行配置,從而可以在有和沒有調(diào)試器構(gòu)建標(biāo)記(build tag)的情況下使用代碼。
注意:
如上所示,設(shè)置標(biāo)簽會(huì)導(dǎo)致性能下降。因此,僅在對(duì)性能要求不高的環(huán)境中使用 -tags=debugger 構(gòu)建的二進(jìn)制文件,或確保通過改善調(diào)試體驗(yàn)來抵消性能損失。
今天就這樣。我們學(xué)習(xí)了如何使用 GoLand 調(diào)試復(fù)雜的 Go 應(yīng)用程序并在 goroutine 中添加標(biāo)簽,從而使生活變得更輕松。
這篇文章中的所有代碼都可以在 github.com/dlsniper/debugger[4] 上找到。用于測(cè)試該庫的示例代碼可在 ?github.com/dlsniper/serverdemo[5] 上找到。
作者:Florin P??an[6]
原文鏈接:https://blog.jetbrains.com/go/2020/03/03/how-to-find-goroutines-during-debugging/
編譯:polaris
參考資料
1867862: https://github.com/go-delve/delve/commit/186786235fc9c2bd9b16c26bb4b0aef60ffb731c
[2]github.com/dlsniper/debugger: https://github.com/dlsniper/debugger
[3]debugger.SetLabels: https://pkg.go.dev/github.com/dlsniper/debugger?tab=doc#SetLabels
[4]github.com/dlsniper/debugger: https://github.com/dlsniper/debugger
[5]github.com/dlsniper/serverdemo: https://github.com/dlsniper/serverdemo
[6]Florin P??an: https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/
推薦閱讀
