列舉幾個(gè) Go 語言常見的坑
點(diǎn)擊上方“Go編程時(shí)光”,選擇“加為星標(biāo)”
第一時(shí)間關(guān)注Go技術(shù)干貨!

我喜歡 Go 語言有幾個(gè)原因:
語言本身極其簡潔(只有 25 個(gè)關(guān)鍵字);
能輕而易舉地實(shí)現(xiàn)交叉編譯;
天然支持創(chuàng)建可靠的 HTTP 服務(wù)器;
從根本上來講,Go 是一種 boring 的語言,可能這就是為什么可以用它來開發(fā)一些諸如 Docker 和 Kubernetes 等很棒的項(xiàng)目,像 Cloudflare 等具有高性能和彈性要求的公司也正在使用它。
盡管上手很容易,但是有很多細(xì)節(jié)還是值得關(guān)注。如果你在不清楚的情況下編寫代碼,很可能會(huì)導(dǎo)致各種稀奇古怪的問題,并且很難發(fā)現(xiàn)和糾正錯(cuò)誤。
下面會(huì)給大家列舉一些常見錯(cuò)誤,是在 review 生產(chǎn)代碼時(shí)發(fā)現(xiàn)的。希望你再遇到相同問題時(shí)能輕松地解決。
HTTP 超時(shí)時(shí)間
HTTP 超時(shí)時(shí)間,其實(shí)在點(diǎn)擊已經(jīng)跟大家討論過這個(gè)問題。但仍然值得再提一提,因?yàn)楹玫慕鉀Q方案總是需要更多的時(shí)間思考的。
使用默認(rèn)的 HTTP 客戶端可以發(fā)出 HTTP 請求,為了說明問題,下面是一個(gè)使用 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è)置超時(shí)時(shí)間,這意味著請求有可能會(huì)被長時(shí)間掛起(ps:具體原因可以查看原文)
所以,解決這個(gè)問題最好的辦法是什么呢?
&http.Client{Timeout: time.Minute},給 HTTP 客戶端定義一個(gè)合理的超時(shí)時(shí)間。你也可以考慮給 HTTP 請求加上 context,這樣做有幾個(gè)好處:
有能力取消正在進(jìn)行的 HTTP 請求;
為一些特殊請求指定超時(shí)時(shí)間;
第 2 個(gè)好處顯得尤為重要,比如你知道有幾個(gè)請求需要耗時(shí)很長時(shí)間,超過 1 個(gè)小時(shí)。但是你又不想每個(gè)請求都設(shè)置這么長的超時(shí)時(shí)間,你就可以只針對特殊請求設(shè)置比較長的超時(shí)時(shí)間。
上面的例子中,如果加上 context 代碼會(huì)像下面這樣:
ctx,?cancel?:=?context.WithTimeout(context.Background(),?time.Minute)
defer?cancel()
req?=?req.WithContext(ctx)
res,?err?:=?c.Do(req)
...
請求時(shí)間如果超過了超時(shí)時(shí)間,c.Do() 調(diào)用就會(huì)返回 DeadlineExceeded 錯(cuò)誤,可以很容易地處理錯(cuò)誤或者重試。
數(shù)據(jù)庫連接
我參與的每一個(gè) Go 項(xiàng)目幾乎都會(huì)出現(xiàn)數(shù)據(jù)庫連接問題。我認(rèn)為對剛?cè)腴T Go 語言的新手來說,有個(gè)難以繞過去的點(diǎn),sql.DB 對象是并發(fā)安全的連接池,而不是單個(gè)數(shù)據(jù)庫連接。這意味著連接使用完之后如果沒有返還給進(jìn)程池,會(huì)輕易導(dǎo)致連接數(shù)耗盡,甚至最后導(dǎo)致應(yīng)用程序宕掉。
例如,數(shù)據(jù)庫連接池包含打開和空閑連接,分別是通過下面這些選項(xiàng)設(shè)置的:
SetConnMaxLifetime,連接可以重用的最長時(shí)間;
SetMaxIdleConns,最大的空閑連接數(shù)量;
SetMaxOpenConns,最大的打開連接數(shù)量;
需要注意的是,即使你的最大打開連接數(shù)設(shè)置成 200,如果連接使用完不返還連接池,應(yīng)用程序也有可能會(huì)耗盡數(shù)據(jù)庫能接受的最大連接數(shù),最后導(dǎo)致宕機(jī)、重啟服務(wù)。你需要檢查數(shù)據(jù)庫設(shè)置,以確保正確設(shè)置了這些參數(shù)。
如果數(shù)據(jù)庫沒有設(shè)置這些參數(shù),應(yīng)用程序?qū)⑤p而易舉地耗盡數(shù)據(jù)庫能接受的連接數(shù)。
讓我們回到進(jìn)程池的問題上,查詢數(shù)據(jù)庫之后,很多開發(fā)人員會(huì)忘記關(guān)閉 *sql.Rows 對象,這就會(huì)導(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ù)庫查詢時(shí)添加超時(shí)時(shí)間的 context。這沒什么問題。
正如上面討論的,我們需要關(guān)閉 rows 對象將連接返還給進(jìn)程池,防止連接數(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)這一點(diǎn)。
goroutine 或者內(nèi)存泄漏
最后一個(gè)要討論的常見問題是 goroutine 泄漏,一般這個(gè)問題難以發(fā)現(xiàn),但通常是由開發(fā)人員的錯(cuò)誤引起的。
使用 channel 時(shí)通常會(huì)發(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() 不返回錯(cuò)誤,我們初始化的 goroutine 將會(huì)掛起直到程序終止。
我們不可能找出每一個(gè)導(dǎo)致 goroutine 泄漏的 地方,我通常采用兩種方法來檢測和消除它們。
第一種方法是在單元測試方法里使用探測器,比如使用 Uber 開源的 goleak 庫,就像下面這個(gè)例子一樣:
func?TestA(t?*testing.T)?{
????defer?goleak.VerifyNone(t)
????//?test?logic?here.
}
這段代碼就會(huì)驗(yàn)證,在代碼優(yōu)美關(guān)閉 30s 之后是否還有多余的 goroutine 在運(yùn)行。
另一種方法是在應(yīng)用程序的運(yùn)行實(shí)例上使用 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)重的泄漏,如果你刷新將會(huì)看到協(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)泄漏位置之后,我仍然建議在單元測試中使用探測器,以確保解決問題。
希望上面討論的這些常見錯(cuò)誤,如果以后你也遇到,可以幫助你更快地識(shí)別并完美地解決問題。
作者:Tyler Finethy
原文:https://medium.com/better-programming/common-go-pitfalls-a92197cd96d2
? ?
--?END?--
???
