goroutine出现未捕获panic导致服务异常终止

1 业务问题与解决

问题描述:
1 发现部分pod存在重启现象,猜测可能是因为接口异常,导致服务进程异常退出
2 查询到相关的panic日志因为特定接口被调用,出现异常,定位上下文
3 定位异常代码
/workspace/git-resource/service/class/class_change.go:101 +0x9a5
    May 21st 2024, 22:37:47.444    beibo_server/manage.GetSmallClassDetail({_, _}, {_, _})
    May 21st 2024, 22:37:47.444        /workspace/git-resource/manage/business_small_class_info.go:147 +0x670
    May 21st 2024, 22:37:47.444    panic: runtime error: index out of range [0] with length 0
    May 21st 2024, 22:37:47.444    library/mq/myaliyunmq.Consume.func1()
    May 21st 2024, 22:37:47.444     - 
    May 21st 2024, 22:37:47.444     - 
    May 21st 2024, 22:37:47.444    created by /library/mq/myaliyunmq.Consume


2 了解go中的panic

在Go语言中,panic是一种用于处理程序错误和异常情况的机制。当程序遇到无法处理的错误或异常时,可以触发panic,并停止当前函数的执行,然后逐层向上返回,直到被recover捕获或程序终止。

1. panic的语法
在Go语言中,可以使用panic关键字来触发panic。其基本语法如下:
panic(value)
value:可以是任意类型的值,表示触发panic时传递的信息。
当程序执行到panic语句时,当前函数的执行将立即停止,任何后续语句都不会被执行,然后控制权将传递给调用该函数的函数。

2. panic的使用场景
panic通常用于以下几种场景:
2.1 不可恢复的错误
当程序遇到无法恢复的错误时,可以使用panic中断程序的正常执行流程。这些错误可能包括非法参数、不可预料的状态或不可修复的运行时错误。通过触发panic,可以停止当前函数的执行并向上传播错误信息。
func divide(a, b int) int { 
    if b == 0 { 
        panic("除数不能为零") 
    } 
    return a / b 
}
2.2 错误处理
在一些特殊情况下,当发生错误时,我们希望程序能够立即停止,并输出错误信息,而不是继续执行下去。这种情况下,可以使用panic来触发错误,然后在程序的顶层函数中使用recover来捕获并处理这些错误。
func main() { 
    defer func() { 
        if err := recover(); err != nil { 
            fmt.Println("发生错误:", err) 
           } 
     }() 
    // 一些可能触发panic的操作 
}
2.3 调试和诊断
在开发和调试过程中,我们有时需要了解程序执行到某个特定点时的状态和变量的值。可以使用panic来暂停程序的执行,以便查看和分析当前状态。
go func debugInfo() { 
    // 一些调试代码 
    panic("调试信息") 
}

3. panic的注意事项
在使用panic时,需要注意以下几点:
panic会导致当前函数的执行立即停止,但defer语句会被正常执行。
如果当前函数中没有捕获panic的recover语句,程序会从当前函数逐层向上返回,并终止程序的执行。
panic传递的信息可以是任意类型的值,通常使用字符串或自定义的错误类型。
可以在defer语句中使用panic,但必须在panic被触发前定义defer语句。
通常建议在顶层函数中使用recover来捕获并处理panic,以防止程序意外终止。
func main() { 
    defer func() { 
        if err := recover(); err != nil { 
            fmt.Println("发生错误:", err) 
            // 执行一些恢复操作 
            } 
    }() 
    // 一些可能触发panic的操作 
}                      
原文链接:https://blog.csdn.net/Freeman_23/article/details/130877598

3 问题复现

复现代码如下:
func test() {

    //下面的代码不会导致服务退出,因为有统一的异常处理规则
    var a int = 1
    var b int = 0
    e := a / b

    // 下面的代码如果没有defer会导致服务退出
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Error("系统异常")
            }
            return
        }()
        var a int = 1
        var b int = 0
        e := a / b
        log.Error("获取页信息列表失败", e)
        //panic("错误")
    }()

    log.Error("获取页信息列表失败", e)
}

为什么非goroutine中的异常不会导致异常退出呢?因为系统里面做了统一的处理
HttpStart()
ptrace.TraceWrapper
otelgin.Middleware(serviceName) 里面有队sapn的处理

4 解决方案与原理

使用goroutine的时候,一定要配合derfer函数中调用recover使用,用于捕获goroutine中的panic
go func() {
    defer func() {
        if err := recover(); err != nil {
            fmt.println(err)
        }
    }()
}()
原理概述:
panic是Go语言中的一个内置函数,可以停止程序的控制流,改变其流转,并且触发“恐慌”事件。recover也是一个内置函数,其功能与panic相反,recover可以让程序重新获取“恐慌”事件后的程序控制权,但是recover必须在defer中才会生效。 
panic会触发延迟调用(defer)。假设当前goroutine中不存在defer,则会直接跳出,也就无法进行recover了。也就是说,在panic时,Go只会在defer中对reocver进行检测。

说明:这里recover并非万能的,它只对用户态下的panic关键字有效,以下代码场景无效:
    m := make(map[int]string)
    for i := 0; i < 1000; i++ {
        go func() {
            defer func() {
                if e := recover(); e != nil {
                    log.Error("代码运行异常", e)
                }
            }()
            m[i] = "编程"
        }()
    }
    log.Info("执行到了吗?")
解析:示例中存在并发写入map的问题,这在Go语言中是不安全的,因为map不是并发安全的。如果您尝试同时从多个goroutine对map进行写入操作,程序可能会发生panic。

为了避免这种情况,您可以使用`sync.Mutex`来同步对map的访问。下面是一个修改后的代码示例,它使用了互斥锁来确保在修改map时只有一个goroutine可以访问它:

```go
package main

import (
    "log"
    "sync"
)

func main() {
    var mutex sync.Mutex
    m := make(map[int]string)
    wg := sync.WaitGroup{}

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            defer func() {
                if e := recover(); e != nil {
                    log.Printf("代码运行异常: %v", e)
                }
            }()
            mutex.Lock()
            m[i] = "编程"
            mutex.Unlock()
        }(i)
    }

    wg.Wait() // 等待所有goroutine完成
    log.Println("执行到了吗?")
}
```

在这个修改后的代码中,我做了以下几点改变:
1. 引入`sync.Mutex`变量`mutex`来控制map的并发访问。
2. 使用`sync.WaitGroup`来等待所有goroutine完成后才继续执行,从而避免主goroutine在子goroutine完成前就结束。
3. 在goroutine的匿名函数中传递`i`作为参数,这样每个goroutine都有自己的索引的副本,防止闭包中的变量捕获问题。
4. `defer wg.Done()`确保每个goroutine在退出前都会调用`Done()`方法,这对于`WaitGroup`来正确等待所有goroutine是必要的。
使用互斥锁可以确保即使有多个goroutine尝试同时写入map,每次也只有一个goroutine可以执行写操作。这样就避免了并发修改导致的panic。而`sync.WaitGroup`确保了主goroutine会等待所有子goroutine完成后才继续执行,这样就可以保证程序在所有数据都写入map之后才结束。

5 项目中的优化点

在myaliyunmq.Consume(mqConsumer, HandleClassChangeMsg)中增加defer和recover函数处理
另外项目中30余次使用goroutine 有二十多次未使用defer和recover函数处理,增加相关的函数处理


参考:
https://www.bilibili.com/read/cv6885200/ 
https://blog.csdn.net/Freeman_23/article/details/130877598

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值