我之前對golang還瞭解的極其膚淺的時候,就已經對goroutine如雷貫耳了,我相信很多同學跟我一樣,會以為在go代碼中,goroutine的身影隨處可見,事實上並不是這樣。
這兩天參與了金融部門的一個小項目,把一個老系統中的小模組從php代碼重構成golang。因為負責重構的同事之前只有php經驗,所以派我和另外一個同事去幫忙。今早總監過來看看進度,無意中看了眼My Code,立刻給我指出了一個嚴重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) //存入緩衝}()