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()
}