为什么无法从父 goroutine 恢复子 goroutine 的 panic
对于这个问题,其实更普遍问题是:为什么无法 recover 其他 goroutine 里产生的 panic?
读者可能会好奇为什么会有人希望从父 goroutine 中恢复子 goroutine 内产生的 panic。这是 因为,如果以下的情况发生在应用程序内,那么整个进程必然退出:
go func() { panic("die die die") }()
当然,上面的代码是显式的 panic,实际情况下,如果不注意编码规范,极有可能触发一些本可以避免的恐慌错误,例如访问越界:
go func() {
a := make([]int, 1)
println(a[1])
}()
2
3
4
发生这种恐慌错误对于服务端开发而言几乎是致命的,因为开发者将无法预测服务的可用性, 只能在错误发生时发现该错误,但这时服务不可用的损失已经产生了。
那么,为什么不能从父 goroutine 中恢复子 goroutine 的 panic?或者一般地说,为什么某个 goroutine 不能捕获其他 goroutine 内产生的 panic?
其实这个问题从 Go 诞生以来就一直被长久地讨论,而答案可以简单地认为是设计使然:因为 goroutine 被设计为一个独立的代码执行单元,拥有自己的执行栈,不与其他 goroutine 共享任何 数据。这意味着,无法让 goroutine 拥有返回值、也无法让 goroutine 拥有自身的 ID 编号等。若 需要与其他 goroutine 产生交互,要么可以使用 channel 的方式与其他 goroutine 进行通信,要么通过共享内存同步方式对共享的内存添加读写锁。
那一点办法也没有了吗?方法自然有,但并不是完美的方法,这里给读者提供一种思路。例如,如果希望有一个全局的恐慌捕获中心,那么可以通过创建一个恐慌通知 channel,并在产生恐慌时,通过 recover 字段将其恢复,并将发生的错误通过 channel 通知给这个全局的恐慌通知器:
func main() {
notifier := make(chan interface{})
startGlobalPanicCapturing(notifier)
Go(func() {
a := make([]int, 1)
fmt.Println(a[1])
}, notifier)
}
func startGlobalPanicCapturing(notifier chan interface{}) {
go func() {
for {
select {
case r := <-notifier:
fmt.Println(r)
}
}
}()
}
func Go(f func(), notifier chan interface{}) {
go func() {
defer func() {
if r := recover(); r != nil {
notifier <- r
}
}()
f()
}()
}
time.Sleep(time.Second * 10)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
上面的 func Go (f func ()) 本质上是对 go 关键字进行了一层封装,确保在执行并发单元前插入一个 defer,从而能够保证恢复一些可恢复的错误。
之所以说这个方案并不完美,原因是如果函数 f 内部不再使用 Go 函数来创建 goroutine,而且含有继续产生必然恐慌的代码,那么仍然会出现不可恢复的情况。
go func() { panic("die die die") }()
读者可能也许会想到,强制某个项目内均使用 Go 函数不就好了?事情也并没有这么简单。因为除了可恢复的错误外,还有一些不可恢复的运行时恐慌(例如并发读写 map),如果这类恐慌一旦发生,那么任何补救都是徒劳的。笔者认为,解决这类问题的根本途径是提高程序员自身对语言的认识,多进行代码测试,以及多通过运维技术来增强容灾机制。