歡迎光臨
每天分享高質量文章

goroutine和panic不得不說的故事

我之前對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) //存入快取
}()

贊(0)

分享創造快樂