我之前對golang還瞭解的極其膚淺的時候,就已經對goroutine如雷貫耳了,我相信很多同學跟我一樣,會以為在go程式碼中,goroutine的身影隨處可見,事實上並不是這樣。
這兩天參與了金融部門的一個小專案,把一個老系統中的小模組從php程式碼重構成golang。因為負責重構的同事之前只有php經驗,所以派我和另外一個同事去幫忙。今早總監過來看看進度,無意中看了眼我的程式碼,立刻給我指出了一個嚴重bug,讓我發現了一個知識盲點,我覺得值得分享一下。
過程
昨天下午寫了一個grpc
介面,根據user_id
從資料庫查詢一張user_config
表,拿到一個city_ids
欄位,是個city_id
組成的字串,然後split
處理後查city
表取城市資料,大概過程類似這樣:
func GetCities(userID int64) ([]*cityData, error) {
var (
strCityIDs string
CityIDs []string
ret []*cityData
)
strCityIDs, _ = userConfig.GetCityIDs(userID) //從user_config表查詢city_id欄位
CityIDs = strings.Split(strCityIDs, sep) //處理成id陣列
err = city.Find(CityIDs, &ret;) //從city表查出資料
return ret, err
}
說白了就是個has_many
關係。因為city
表幾乎不會變化,早上來了公司,我覺得可以加個快取,所以改成了:
func GetCities(userID int64) ([]*cityData, error) {
var (
strCityIDs string
CityIDs []string
ret []*cityData
)
strCityIDs, _ = userConfig.GetCityIDs(userID) //從user_config表查詢city_id欄位
err := cache.Get(prefix+strCityIDs, &ret;) //先從快取拿資料
if err == nil {
return ret, nil
}
CityIDs = strings.Split(strCityIDs, sep) //處理成id陣列
err = city.Find(CityIDs, &ret;) //從city表查出資料
if err == nil {
ok := cache.Set(prefix+strCityIDs, &ret;, 12*time.Hour) //存入快取
if !ok {
doNothing()
}
}
return ret, err
}
改完後“靈機”一動,想起自己幾乎沒在公司專案中看到過go
關鍵字的出現,自己也基本沒在生產中實際用過goroutine
,於是把cache.Set
改成了go cache.Set
。我覺得存入快取成功與否並不影響主流程(即便失敗其實我也什麼都不做),所以完全可以交給協程去做,而且這樣主goroutine
可以傳回的更快。 這時總監過來了。 聊了兩句,突然指著程式碼跟我說:“這裡不對,不能用協程!” 我:“為啥啊?” 總監:“因為協程裡面發生panic會讓整個行程crash。” 我更加迷惑了:“但是我在middleware裡加了recover啊,會抓到panic的。” middleware
程式碼:
func (*Interceptor) Method(ctx context.Context, srvInfo *core.SrvInfo, req interface{}, handler func(context.Context, interface{}) (interface{}, error)) (ret interface{}, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
ret, err = handler(ctx, req) //所有下層邏輯全部在這個函式裡分發,所以我錯誤地認為任何panic都能在這裡recover
return ret, err
}
總監:“goroutine發生panic,只有自身能夠recover,其它goroutine是抓不到的,這是常識啊。” 我:“……” 嚇的我啥也沒敢再說,趕緊把go
關鍵字刪了,然後等總監走了之後,立馬上網研究了一波goroutine、panic、recover之間的關係,下麵是結論。
結論
首先,要明確一點,panic
會停止整個行程,不僅僅是當前goroutine
,也就是說整個程式都會涼涼(我現在認為這就是goroutine
沒有在程式碼裡泛濫的原因之一,另外的原因是,我覺得在cpu
核全部跑起來的情況下,開再多的goroutine
也只能併發而不能並行)。 其次,panic
是有序的、可控的停止程式,不是啪唧一下就宕掉了,所以我們還可以用recover
補救。 然後,recover
只能在defer
裡面生效,如果不是在defer
裡呼叫,會直接傳回nil
。 最後,很重要的一點是:goroutine
發生panic
時,只會呼叫自身的defer
,所以即便主goroutine
裡寫了recover
邏輯,也無法拯救到其它goroutine
裡的panic
。 所以呢,之前的go cache.Set
寫法是很危險的,因為cache
裡沒有做任何recover
,一旦出現panic
,會影響到整個系統。 假設我一定裝這個逼用go
關鍵字實現(顯然我不是這樣的人),程式碼可以改成:
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("don't worry, I can take care of myself")
}
}()
cache.Set(prefix+strCityIDs, &ret;, 12*time.Hour) //存入快取
}()