一篇文章彻底搞懂 Panic/Recover

91 阅读2分钟

panic/recover 是 Go 的错误处理方式之一,不同于其他语言的 try/catch,Go 对 panic 的使用是很克制的,只有在一些极少数的场景下才会使用 panic,这并不是说 panic 不重要,相反对 panic/recover的合理使用会极大的提高我们程序的健壮性

首先在程序完全退出之前会有一个 exiting phase 阶段,在这个阶段里,所有的 defer 函数会安装压入栈的相反顺序来执行,当所有函数执行结束以后,程序也就完全退出了。通常一个调用会在下面三种情况下进入 exiting phase 阶段或者直接退出

  • 调用正常返回
  • 在调用中出现 panic
  • 在调用中调用 runtime.Goexit 函数并完全退出后

当调用中出现 panic 的时候,这个 panic 就和调用关联起来了,runtime.Goexit 的调用也有同样的效果,在同一时刻,一个函数调用最多只能和一个未恢复的 panic 关联,如果当 panic 恢复以后,调用就不会和 panic 进行关联,如果当一个新的 panic 在调用中发生,那么新的 panic 就会取代老的 panic 和调用进行关联,嵌套调用中未恢复的 panic 也会向上层传播

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println(recover()) // 最后只有 3 会被恢复
	}()
	
	defer panic(3) // will replace panic 2
	defer panic(2) // will replace panic 1
	defer panic(1) // will replace panic 0
	panic(0)
}

所以当一个 goroutine 退出的时候如果和一个未恢复的 panic 关联,那么整个程序就会崩溃

package main

func main() {
    // The new goroutine.
    go func() {
       // panic 2 会传播到最上层
       defer func() {
          // panic 2 取代 1
          defer panic(2)

          // panic1 向上层传播
          func() {
             // 两个 panic 同时存在,1 会取代 0
             panic(1)
          }()
       }()
       panic(0)
    }()

    select {}
}

对于 recover 来说,它的生效条件是它必须被直接调用的延迟函数所包裹,并且这个延迟函数的直接调用者必须与当前 Goroutine 中的最新未恢复 panic 相关联

import "fmt"

func demo() {
    defer func() {
       defer func() {
          fmt.Println(recover()) // 1
       }()

       defer fmt.Println(recover()) //  2

       panic(2)
    }()
    panic(1)
}

func main() {
    demo()
}