作为Go开发者,我们都知道使用recover()可以捕获异常,防止程序崩溃。但你可能不知道,有些异常是recover()也无法捕获的"致命杀手"!
在一次线上事故中,某部门的服务因为一个简单的编程错误导致崩溃,尽管他们已经在代码中使用了recover()进行保护。
这让他们意识到,很多Go开发者对recover()的能力存在误解。
下面就来深入探讨哪些异常可以被recover()捕获,而哪些不能,以及为什么。
哪些异常可以被Recover捕获?
在Go语言中,以下常见的异常情况是可以通过recover()捕获的:
1. 切片或数组下标越界
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err)
}
}()
nums := []int{}
fmt.Println(nums[1]) // 越界访问,触发panic
}
2. 空指针解引用
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err)
}
}()
var str *string
fmt.Println(*str) // 空指针解引用
}
3. 未初始化的map进行写操作
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err)
}
}()
var mp map[string]int
mp[""] = 1 // 对未初始化的map进行写操作
}
4. 类型断言失败
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("panic")
}
}()
var str interface{} = "123"
_ = str.(int) // 类型断言失败
}
这些异常都是通过runtime.panic()抛出的,因此可以被recover()捕获。
无法被Recover捕获的"致命异常"
以下是我们今天的重点——那些即使使用了recover()也无法捕获的致命异常:
1. 并发读写Map(最常见!)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err) // 这行代码不会执行
}
}()
m := map[string]int{}
// goroutine 1: 写操作
go func() {
for {
m["x"] = 1
}
}()
// goroutine 2: 读操作
for {
_ = m["y"]
}
}
当Go检测到map的并发读写时,会抛出fatal error: concurrent map writes,这是一个不可恢复的异常。
为什么无法恢复? 因为当检测到数据竞争时,map的内部结构可能已经被破坏,继续运行可能导致不可预知的结果。
2. 内存溢出(OOM)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err) // 这行代码不会执行
}
}()
_ = make([]int64, 1<<40) // 尝试分配超大内存
}
内存不足会导致程序无法继续执行,这种情况下的异常无法被恢复。
3. 栈内存耗尽
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err) // 这行代码不会执行
}
}()
var fun func([1000]int64)
fun = func(i [1000]int64) {
fun(i) // 无限递归,导致栈溢出
}
fun([1000]int64{})
}
无限递归会导致栈空间耗尽,报错:runtime: goroutine stack exceeds 1000000000-byte limit。
4. 启动nil函数
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err) // 这行代码不会执行
}
}()
var f func()
go f() // 启动一个nil函数
}
这会导致:fatal error: go of nil func value。
5. 程序死锁
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("err:", err) // 这行代码不会执行
}
}()
// 所有goroutine都处于休眠状态,产生死锁
<-make(chan int)
}
当所有goroutine都处于休眠状态时,Go运行时会检测到死锁并抛出致命错误。
为什么这些异常无法被Recover捕获?
本质区别在于异常的产生机制:
- 可恢复的异常:通过
runtime.panic()抛出 - 不可恢复的异常:通过
runtime.throw()或runtime.fatal()抛出,这些属于系统级致命错误
不可恢复的异常通常意味着程序状态已经严重损坏,继续运行可能会导致更严重的问题。
如何预防这些致命异常?
1. 并发安全编程
- 使用
sync.Mutex或sync.RWMutex保护共享数据 - 使用
sync.Map代替普通map进行并发读写 - 使用channel进行goroutine间通信
2. 资源控制
- 限制递归深度,避免无限递归
- 预估切片和map的容量,避免频繁扩容
- 对大数据集使用流式处理
3. 测试与检测
- 使用
go test -race进行竞态检测 - 进行压力测试,模拟高并发场景
- 编写单元测试覆盖边界条件
写在最后
虽然recover()是Go中强大的错误处理机制,但它不是万能的。对于并发读写map、内存溢出、栈溢出等致命错误,recover()也无能为力。
正确的做法是:预防优于补救。通过良好的编程习惯、严格的测试和适当的资源控制,避免触发这些不可恢复的异常,才是保证程序稳定性的根本之道。