goroutine与panic不得不说的故事

作者: df09dc0ad66f | 来源:发表于2018-08-15 14:28 被阅读5次

    我之前对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) //存入缓存
    }()
    

    相关文章

      网友评论

      • 王巍瑾_三月:我遇到的功能也是缓存,刚要这么写,看到有人推送的这篇文章,吓得我赶紧不写了,感谢

      本文标题:goroutine与panic不得不说的故事

      本文链接:https://www.haomeiwen.com/subject/ikzibftx.html