Go 語言常見的坑
via:
https://medium.com/better-programming/common-go-pitfalls-a92197cd96d2
作者:Tyler Finethy
四哥水平有限,如有翻譯或理解錯誤,煩請幫忙指出,感謝!
原文如下:
我喜歡 Go 語言有幾個原因:
語言本身極其簡潔(只有 25 個關(guān)鍵字);
能輕而易舉地實現(xiàn)交叉編譯;
天然支持創(chuàng)建可靠的 HTTP 服務(wù)器;
從根本上來講,Go 是一種 boring 的語言,可能這就是為什么可以用它來開發(fā)一些諸如 Docker 和 Kubernetes 等很棒的項目,像 Cloudflare 等具有高性能和彈性要求的公司也正在使用它。
盡管上手很容易,但是有很多細(xì)節(jié)還是值得關(guān)注。如果你在不清楚的情況下編寫代碼,很可能會導(dǎo)致各種稀奇古怪的問題,并且很難發(fā)現(xiàn)和糾正錯誤。
下面會給大家列舉一些常見錯誤,是在 review 生產(chǎn)代碼時發(fā)現(xiàn)的。希望你再遇到相同問題時能輕松地解決。
HTTP 超時時間
HTTP 超時時間,其實在點擊已經(jīng)跟大家討論過這個問題。但仍然值得再提一提,因為好的解決方案總是需要更多的時間思考的。
使用默認(rèn)的 HTTP 客戶端可以發(fā)出 HTTP 請求,為了說明問題,下面是一個使用 GET 請求訪問 google.com 的例子:
package?main
import?(
????"io/ioutil"
????"log"
????"net/http"
)
var?(
????c?=?&http.Client{}
)
func?main()?{
????req,?err?:=?http.NewRequest("GET",?"google.com",?nil)
????if?err?!=?nil?{
????????log.Fatal(err)
????}
????res,?err?:=?c.Do(req)
????if?err?!=?nil?{
????????log.Fatal(err)
????}
????defer?res.Body.Close()
????b,?_?:=?ioutil.ReadAll(res.Body)
????...
}
正如文章指出的,默認(rèn)的 HTTP 客戶端沒有設(shè)置超時時間,這意味著請求有可能會被長時間掛起(ps:具體原因可以查看原文)
所以,解決這個問題最好的辦法是什么呢?
&http.Client{Timeout: time.Minute},給 HTTP 客戶端定義一個合理的超時時間。你也可以考慮給 HTTP 請求加上 context,這樣做有幾個好處:
有能力取消正在進行的 HTTP 請求;
為一些特殊請求指定超時時間;
第 2 個好處顯得尤為重要,比如你知道有幾個請求需要耗時很長時間,超過 1 個小時。但是你又不想每個請求都設(shè)置這么長的超時時間,你就可以只針對特殊請求設(shè)置比較長的超時時間。
上面的例子中,如果加上 context 代碼會像下面這樣:
ctx,?cancel?:=?context.WithTimeout(context.Background(),?time.Minute)
defer?cancel()
req?=?req.WithContext(ctx)
res,?err?:=?c.Do(req)
...
請求時間如果超過了超時時間,c.Do() 調(diào)用就會返回 DeadlineExceeded 錯誤,可以很容易地處理錯誤或者重試。
數(shù)據(jù)庫連接
我參與的每一個 Go 項目幾乎都會出現(xiàn)數(shù)據(jù)庫連接問題。我認(rèn)為對剛?cè)腴T Go 語言的新手來說,有個難以繞過去的點,sql.DB 對象是并發(fā)安全的連接池,而不是單個數(shù)據(jù)庫連接。這意味著連接使用完之后如果沒有返還給進程池,會輕易導(dǎo)致連接數(shù)耗盡,甚至最后導(dǎo)致應(yīng)用程序宕掉。
例如,數(shù)據(jù)庫連接池包含打開和空閑連接,分別是通過下面這些選項設(shè)置的:
SetConnMaxLifetime,連接可以重用的最長時間;
SetMaxIdleConns,最大的空閑連接數(shù)量;
SetMaxOpenConns,最大的打開連接數(shù)量;
需要注意的是,即使你的最大打開連接數(shù)設(shè)置成 200,如果連接使用完不返還連接池,應(yīng)用程序也有可能會耗盡數(shù)據(jù)庫能接受的最大連接數(shù),最后導(dǎo)致宕機、重啟服務(wù)。你需要檢查數(shù)據(jù)庫設(shè)置,以確保正確設(shè)置了這些參數(shù)。
如果數(shù)據(jù)庫沒有設(shè)置這些參數(shù),應(yīng)用程序?qū)⑤p而易舉地耗盡數(shù)據(jù)庫能接受的連接數(shù)。
讓我們回到進程池的問題上,查詢數(shù)據(jù)庫之后,很多開發(fā)人員會忘記關(guān)閉 *sql.Rows 對象,這就會導(dǎo)致超出最大連接數(shù)限制,并導(dǎo)致死鎖或者高延遲。下面給大家展示下類似的代碼片段:
package?main
import?(
????"context"
????"database/sql"
????"fmt"
????"log"
)
var?(
????ctx?context.Context
????db??*sql.DB
)
func?main()?{
????age?:=?27
????ctx,?cancel?:=?context.WithTimeout(context.Background(),?time.Minute)
????defer?cancel()
????rows,?err?:=?db.QueryContext(ctx,?"SELECT?name?FROM?users?WHERE?age=?",?age)
????if?err?!=?nil?{
????????log.Fatal(err)
????}
????for?rows.Next()?{
????????var?name?string
????????if?err?:=?rows.Scan(&name);?err?!=?nil?{
????????????log.Fatal(err)
????????}
????????fmt.Println(name)
????}
????...
}
相信你也注意到,正如能在 HTTP 請求上添加 context 一樣,我們也可以在數(shù)據(jù)庫查詢時添加超時時間的 context。這沒什么問題。
正如上面討論的,我們需要關(guān)閉 rows 對象將連接返還給進程池,防止連接數(shù)超出。
rows,?err?:=?db.QueryContext(ctx,?"SELECT?name?FROM?users?WHERE?age=?",?age)
if?err?!=?nil?{
????log.Fatal(err)
}
defer?rows.Close()
如果在函數(shù)或者包之間傳遞數(shù)據(jù)庫連接,尤其難以發(fā)現(xiàn)這一點。
goroutine 或者內(nèi)存泄漏
最后一個要討論的常見問題是 goroutine 泄漏,一般這個問題難以發(fā)現(xiàn),但通常是由開發(fā)人員的錯誤引起的。
使用 channel 時通常會發(fā)生這種問題,比如:
package?main
func?main()?{
????c?:=?make(chan?error)
????go?func()?{
????????for?err?:=?range?c?{
????????????if?err?!=?nil?{
????????????????panic(err)
????????????}
????????}
????}()
????c?<-?someFunc()
????...
}
如果我們不關(guān)閉通道 c 或者 someFunc() 不返回錯誤,我們初始化的 goroutine 將會掛起直到程序終止。
我們不可能找出每一個導(dǎo)致 goroutine 泄漏的 地方,我通常采用兩種方法來檢測和消除它們。
第一種方法是在單元測試方法里使用探測器,比如使用 Uber 開源的 goleak 庫,就像下面這個例子一樣:
func?TestA(t?*testing.T)?{
????defer?goleak.VerifyNone(t)
????//?test?logic?here.
}
這段代碼就會驗證,在代碼優(yōu)美關(guān)閉 30s 之后是否還有多余的 goroutine 在運行。
另一種方法是在應(yīng)用程序的運行實例上使用 Go profiler,并查看存活的 goroutine 數(shù)量。其中一種方法就是使用 net/http/pprof 庫,并查看生成的火焰圖。
就像下面這樣使用它:
import?_?"net/http/pprof"
func?someFunc()?{
????go?func()?{
????????log.Println(http.ListenAndServe("localhost:6060",?nil))
????}
}
上面這段代碼,pprof 占用 6060 端口,對于特別嚴(yán)重的泄漏,如果你刷新將會看到協(xié)程數(shù)量在增多;對于更多的一些微小泄漏問題,則需要查看 profile 發(fā)現(xiàn)具體的問題,profile 頁面就像下面這樣:
goroutine?profile:?total?39
2?@?0x43cf10?0x44ca6b?0x980600?0x46b301
#????0x9805ff????database/sql.(*DB).connectionCleaner+0x36f??/usr/local/go/src/database/sql/sql.go:950
2?@?0x43cf10?0x44ca6b?0x980b18?0x46b301
#????0x980b17????database/sql.(*DB).connectionOpener+0xe7????/usr/local/go/src/database/sql/sql.go:1052
2?@?0x43cf10?0x44ca6b?0x980c4b?0x46b301
#????0x980c4a????database/sql.(*DB).connectionResetter+0xfa??/usr/local/go/src/database/sql/sql.go:1065
...
如果你的應(yīng)用程序是空閑的,但是你又看見大數(shù)據(jù)量的 goroutine,這說明程序已經(jīng)有問題了。確認(rèn)泄漏位置之后,我仍然建議在單元測試中使用探測器,以確保解決問題。
總結(jié)
希望上面討論的這些常見錯誤,如果以后你也遇到,可以幫助你更快地識別并完美地解決問題。
推薦閱讀
福利 我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復(fù)?ebook?獲取;還可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。
