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