goroutine与panic不得不说的故事

柔顺的灵魂 · · 767 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

我之前对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不得不说的故事

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:812540095

767 次点击  
加入收藏 微博
2 回复  |  直到 2019-07-08 02:02:12
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传