送给水深火热的 Gopher 们的解药
看了看日歷,現(xiàn)在已經(jīng)是 2021 年了,偶爾還是能看到有人在發(fā)諸如 《http body 未關(guān)閉導(dǎo)致線上事故》,或者 《sql.Rows 未關(guān)閉半夜驚魂》類的文章,令人有一種夢回 2015 的感覺。
在這個(gè) Go 的靜態(tài)分析工具已經(jīng)強(qiáng)到爛大街的時(shí)代,寫這些文章除了暴露這些人所在的公司基礎(chǔ)設(shè)施比較差,代碼質(zhì)量低以外,并不能體現(xiàn)出什么其它的意思了。畢竟哪怕是不懂怎么讀源碼,這樣的問題你 Google 搜一下也知道是怎么回事了。
特別是有些人還掛著大公司的 title,讓人更加不能理解了。下面是簡單的靜態(tài)分析工具的科普,希望給那些還在水深火熱的 Gopher 們送點(diǎn)解藥。
何謂靜態(tài)分析
靜態(tài)分析是通過掃描并解析用戶代碼,尋找代碼中的潛在 bug 的一種手段。
靜態(tài)分析一般會集成在項(xiàng)目上線的 CI 流程中,如果分析過程找到了 bug,會直接阻斷上線,避免有問題的代碼被部署到線上系統(tǒng)。從而在部署早期發(fā)現(xiàn)并修正潛在的問題。
社區(qū)常見 linter
時(shí)至今日,社區(qū)已經(jīng)有了豐富的 linter 資源供我們使用,本文會挑出一些常見 linter 進(jìn)行說明。
go lint
go lint 是官方出的 linter,是 Go 語言最早期的 linter 了,其可以檢查:
導(dǎo)出函數(shù)是否有注釋
變量、函數(shù)、包命名不符合 Go 規(guī)范,有下劃線
receiver 命名是否不符合規(guī)范
但這幾年社區(qū)的 linter 蓬勃發(fā)展,所以這個(gè)項(xiàng)目也被官方 deprecated 掉了。其主要功能被另外一個(gè) linter:revive[^1] 完全繼承了。
go vet
go vet 也是官方提供的靜態(tài)分析工具,其內(nèi)置了鎖拷貝檢查、循環(huán)變量捕獲問題、printf 參數(shù)不匹配等工具。
比如新手老手都很容易犯的 loop capture 錯(cuò)誤:
package?mainfunc?main()?{var?a?=?map[int]int?{1?:?1,?2:?3}var?b?=?map[int]*int{}for?k,?r?:=?range?a?{go?func()?{b[k]?=?&r}()} }go vet 會直接把你罵醒:
~/test?git:master?????go?vet?./clo.go #?command-line-arguments ./clo.go:8:6:?loop?variable?k?captured?by?func?literal ./clo.go:8:12:?loop?variable?r?captured?by?func?literal執(zhí)行 go tool vet help 可以看到 go vet 已經(jīng)內(nèi)置的一些 linter。
~?????go?tool?vet?help vet?is?a?tool?for?static?analysis?of?Go?programs.vet?examines?Go?source?code?and?reports?suspicious?constructs, such?as?Printf?calls?whose?arguments?do?not?align?with?the?format string.?It?uses?heuristics?that?do?not?guarantee?all?reports?are genuine?problems,?but?it?can?find?errors?not?caught?by?the?compilers.Registered?analyzers:asmdecl??????report?mismatches?between?assembly?files?and?Go?declarationsassign???????check?for?useless?assignmentsatomic???????check?for?common?mistakes?using?the?sync/atomic?packagebools????????check?for?common?mistakes?involving?boolean?operatorsbuildtag?????check?that?+build?tags?are?well-formed?and?correctly?locatedcgocall??????detect?some?violations?of?the?cgo?pointer?passing?rulescomposites???check?for?unkeyed?composite?literalscopylocks????check?for?locks?erroneously?passed?by?valueerrorsas?????report?passing?non-pointer?or?non-error?values?to?errors.Ashttpresponse?check?for?mistakes?using?HTTP?responsesloopclosure??check?references?to?loop?variables?from?within?nested?functionslostcancel???check?cancel?func?returned?by?context.WithCancel?is?callednilfunc??????check?for?useless?comparisons?between?functions?and?nilprintf???????check?consistency?of?Printf?format?strings?and?argumentsshift????????check?for?shifts?that?equal?or?exceed?the?width?of?the?integerstdmethods???check?signature?of?methods?of?well-known?interfacesstructtag????check?that?struct?field?tags?conform?to?reflect.StructTag.Gettests????????check?for?common?mistaken?usages?of?tests?and?examplesunmarshal????report?passing?non-pointer?or?non-interface?values?to?unmarshalunreachable??check?for?unreachable?codeunsafeptr????check?for?invalid?conversions?of?uintptr?to?unsafe.Pointerunusedresult?check?for?unused?results?of?calls?to?some?functions默認(rèn)情況下這些 linter 都是會跑的,當(dāng)前很多 IDE 在代碼修改時(shí)會自動(dòng)執(zhí)行 go vet,所以我們在寫代碼的時(shí)候一般就能發(fā)現(xiàn)這些錯(cuò)了。
但?go vet?還是應(yīng)該集成到線上流程中,因?yàn)橛行┏绦騿T的下限實(shí)在太低。
errcheck
Go 語言中的大多數(shù)函數(shù)返回字段中都是有 error 的:
func?sayhello(wr?http.ResponseWriter,?r?*http.Request)?{io.WriteString(wr,?"hello") }func?main()?{http.HandleFunc("/",?sayhello)http.ListenAndServe(":1314",?nil)?//?這里返回的?err?沒有處理 }這個(gè)例子中,我們沒有處理?http.ListenAndServe?函數(shù)返回的 error 信息,這會導(dǎo)致我們的程序在啟動(dòng)時(shí)發(fā)生靜默失敗。
程序員往往會基于過往經(jīng)驗(yàn),對當(dāng)前的場景產(chǎn)生過度自信,從而忽略掉一些常見函數(shù)的返回錯(cuò)誤,這樣的編程習(xí)慣經(jīng)常為我們帶來意外的線上事故。例如,規(guī)矩的寫法是下面這樣的:
data,?err?:=?getDataFromRPC() if?err?!=?nil?{return?nil,?err }//?do?business?logic age?:=?data.age而自信的程序員可能會寫成這樣:
data,?_?:=?getDataFromRPC()//?do?business?logic age?:=?data.age如果底層 RPC 邏輯出錯(cuò),上層的 data 是個(gè)空指針也是很正常的,如果底層函數(shù)返回的 err 非空時(shí),我們不應(yīng)該對其它字段做任何的假設(shè)。這里 data 完全有可能是個(gè)空指針,造成用戶程序 panic。
errcheck 會強(qiáng)制我們在代碼中檢查并處理 err。
gocyclo
gocyclo 主要用來檢查函數(shù)的圈復(fù)雜度。圈復(fù)雜度可以參考下面的定義:
圈復(fù)雜度(Cyclomatic complexity)是一種代碼復(fù)雜度的衡量標(biāo)準(zhǔn),在 1976 年由 Thomas J. McCabe, Sr. 提出。在軟件測試的概念里,圈復(fù)雜度用來衡量一個(gè)模塊判定結(jié)構(gòu)的復(fù)雜程度,數(shù)量上表現(xiàn)為線性無關(guān)的路徑條數(shù),即合理的預(yù)防錯(cuò)誤所需測試的最少路徑條數(shù)。圈復(fù)雜度大說明程序代碼可能質(zhì)量低且難于測試和維護(hù),根據(jù)經(jīng)驗(yàn),程序的可能錯(cuò)誤和高的圈復(fù)雜度有著很大關(guān)系。
看定義較為復(fù)雜但計(jì)算還是比較簡單的,我們可以認(rèn)為:
一個(gè) if,圈復(fù)雜度?+ 1
一個(gè) switch 的 case,圈復(fù)雜度?+ 1
一個(gè) for 循環(huán),圈復(fù)雜度 + 1
一個(gè) && 或 ||,圈復(fù)雜度 + 1
在大多數(shù)語言中,若函數(shù)的圈復(fù)雜度超過了 10,那么我們就認(rèn)為該函數(shù)較為復(fù)雜,需要做拆解或重構(gòu)。部分場景可以使用表驅(qū)動(dòng)的方式進(jìn)行重構(gòu)。
由于在 Go 語言中,我們使用?if err != nil?來處理錯(cuò)誤,所以在一個(gè)函數(shù)中出現(xiàn)多個(gè)?if err != nil?是比較正常的,因此 Go 中函數(shù)復(fù)雜度的閾值可以稍微調(diào)高一些,15 是較為合適的值。
下面是在個(gè)人項(xiàng)目 elasticsql 中執(zhí)行 gocyclo 的結(jié)果,輸出 top 10 復(fù)雜的函數(shù):
~/g/s/g/c/elasticsql?git:master?????gocyclo?-top?10??./ 23?elasticsql?handleSelectWhere?select_handler.go:289:1 16?elasticsql?handleSelectWhereComparisonExpr?select_handler.go:220:1 16?elasticsql?handleSelect?select_handler.go:11:1 9?elasticsql?handleGroupByFuncExprDateHisto?select_agg_handler.go:82:1 9?elasticsql?handleGroupByFuncExprDateRange?select_agg_handler.go:154:1 8?elasticsql?buildComparisonExprRightStr?select_handler.go:188:1 7?elasticsql?TestSupported?select_test.go:80:1 7?elasticsql?Convert?main.go:28:1 7?elasticsql?handleGroupByFuncExpr?select_agg_handler.go:215:1 6?elasticsql?handleSelectWhereOrExpr?select_handler.go:157:1bodyclose
使用 bodyclose[^2] 可以幫我們檢查在使用 HTTP 標(biāo)準(zhǔn)庫時(shí)忘記關(guān)閉 http body 導(dǎo)致連接一直被占用的問題。
resp,?err?:=?http.Get("http://example.com/")?//?Wrong?case if?err?!=?nil?{//?handle?error } body,?err?:=?ioutil.ReadAll(resp.Body)像上面這樣的例子是不對的,使用標(biāo)準(zhǔn)庫很容易犯這樣的錯(cuò)。bodyclose 可以直接檢查出這個(gè)問題:
#?command-line-arguments ./httpclient.go:10:23:?response?body?must?be?closed所以必須要把 Body 關(guān)閉:
resp,?err?:=?http.Get("http://example.com/") if?err?!=?nil?{//?handle?error } defer?resp.Body.Close()?//?OK body,?err?:=?ioutil.ReadAll(resp.Body)HTTP 標(biāo)準(zhǔn)庫的 API 設(shè)計(jì)的不太好,這個(gè)問題更好的避免方法是公司內(nèi)部將 HTTP client 封裝為 SDK,防止用戶寫出這樣不 Close HTTP body 的代碼。
sqlrows
與 HTTP 庫設(shè)計(jì)類似,我們在面向數(shù)據(jù)庫編程時(shí),也會碰到 sql.Rows 忘記關(guān)閉的問題,導(dǎo)致連接大量被占用。sqlrows[^3] 這個(gè) linter 能幫我們避免這個(gè)問題,先來看看錯(cuò)誤的寫法:
rows,?err?:=?db.QueryContext(ctx,?"SELECT?*?FROM?users") if?err?!=?nil?{return?nil,?err }for?rows.Next()?{err?=?rows.Scan(...)if?err?!=?nil?{return?nil,?err?//?NG:?this?return?will?not?release?a?connection.} }正確的寫法需要在使用完后關(guān)閉 sql.Rows:
rows,?err?:=?db.QueryContext(ctx,?"SELECT?*?FROM?users") defer?rows.Close()?//?NG:?using?rows?before?checking?for?errors if?err?!=?nil?{return?nil,?err }與 HTTP 同理,公司內(nèi)也應(yīng)該將 DB 查詢封裝為合理的 SDK,不要讓業(yè)務(wù)使用標(biāo)準(zhǔn)庫中的 API,避免上述錯(cuò)誤發(fā)生。
funlen
funlen[^4] 和 gocyclo 類似,但是這兩個(gè) linter 對代碼復(fù)雜度的視角不太相同,gocyclo 更多關(guān)注函數(shù)中的邏輯分支,而 funlen 則重點(diǎn)關(guān)注函數(shù)的長度。默認(rèn)函數(shù)超過 60 行和 40 條語句時(shí),該 linter 即會報(bào)警。
linter 集成工具
一個(gè)一個(gè)去社區(qū)里找 linter 來拼搭效率太低,當(dāng)前社區(qū)里已經(jīng)有了較好的集成工具,早期是 gometalinter,后來性能更好,功能更全的 golangci-lint 逐漸取而代之。目前 golangci-lint 是 Go 社區(qū)的絕對主流 linter。
golangci-lint
golangci-lint[^5] 能夠通過配置來 enable 很多 linter,基本主流的都包含在內(nèi)了。
在本節(jié)開頭講到的所有 linter 都可以在 golangci-lint 中進(jìn)行配置,
使用也較為簡單,只要在項(xiàng)目目錄執(zhí)行 golangci-lint run . 即可。
~/g/s/g/c/elasticsql?git:master?????golangci-lint?run?. main.go:36:9:?S1034:?assigning?the?result?of?this?type?assertion?to?a?variable?(switch?stmt?:=?stmt.(type))?could?eliminate?type?assertions?in?switch?cases?(gosimple)switch?stmt.(type)?{^ main.go:38:34:?S1034(related?information):?could?eliminate?this?type?assertion?(gosimple)dsl,?table,?err?=?handleSelect(stmt.(*sqlparser.Select))^ main.go:40:23:?S1034(related?information):?could?eliminate?this?type?assertion?(gosimple)return?handleUpdate(stmt.(*sqlparser.Update))^ main.go:42:23:?S1034(related?information):?could?eliminate?this?type?assertion?(gosimple)return?handleInsert(stmt.(*sqlparser.Insert))^ select_handler.go:192:9:?S1034:?assigning?the?result?of?this?type?assertion?to?a?variable?(switch?expr?:=?expr.(type))?could?eliminate?type?assertions?in?switch?cases?(gosimple)switch?expr.(type)?{參考資料
[1] https://revive.run/
[2] https://github.com/timakin/bodyclose
[3] https://github.com/gostaticanalysis/sqlrows
[4] https://github.com/ultraware/funlen
[5] https://github.com/golangci/golangci-lint
總結(jié)
以上是生活随笔為你收集整理的送给水深火热的 Gopher 们的解药的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 曹大带我学 Go(9)—— 开始积累自己
- 下一篇: MQ 正在变成臭水沟