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不得不说的故事

    我之前对golang还了解的极其肤浅的时候,就已经对goroutine如雷贯耳了,我相信很多同学跟我一样,会以为在...

  • goroutine与panic

    如果某个goroutine panic了,而且这个goroutine里面没有捕获(recover),那么整个进程就...

  • Goroutine panic

    https://zhuanlan.zhihu.com/p/42101856?utm_source=wechat_s...

  • Golang语法系列——recover异常处理

    现象 panic 只会触发当前 Goroutine 的 defer; recover 只有在 defer 中调用才...

  • goroutine配上panic会怎样?

    我是一个着迷于产品和运营的技术人,乐于跨界的终身学习者。欢迎关注我的个人公众号「跨界架构师」 每周五11:45 按...

  • golang error与panic处理

    error与panic error:可预见的错误 panic:不可预见的异常 panic处理 通过panic,de...

  • 05-内建方法-panic/recover

    1. panic 单独使用panic,发现错误后程序退出 1.1 代码 1.2 结果 一个悲伤的故事,panic ...

  • 与2017不得不说的故事

    2017年最后一天,你好间隔364天,我们见面了仿佛昨天才刚和16年告别,今天,就要和你告别了时间的相册,记录着我...

  • Go sync.WaitGroup

    问题 当main goroutine为了等待work goroutine都运行完毕,不得不在程序末尾使用time....

  • 聊聊golang的panic与recover

    序 本文主要研究一下golang的panic与recover panic与recover recover在如下三种...

网友评论

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

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

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