首页 为什么父 goroutine 不能恢复子 goroutine 的 panic?
文章
取消

为什么父 goroutine 不能恢复子 goroutine 的 panic?

先回忆下几个 panic 情形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 主动 panic
// panic: bad condition
panic("bad condition")


// 下标越界
// panic: runtime error: index out of range [1] with length 0
m := make([]int, 0)
fmt.Println(m[1])

// 关闭 nil 的 channel
// panic: close of nil channel
var ch chan int
close(ch)

// 断言失败
// panic: interface conversion: interface {} is int, not string
var v interface{} = 10
fmt.Println(v.(string))

如题,为什么父 goroutine 不能恢复子 goroutine 的 panic?换句话说更普遍的问题是为什么不能 recover 其他 goroutine 里产生的 panic?

这个问题是一个有争议的问题,讨论过很久,问题的原因可以简单地认为是设计问题:goroutine 被设计为一个独立的代码执行单元,拥有自己的执行栈,不与其它 goroutine 共享任何数据。这意味着,不能让 goroutine 拥有返回值,也无法让 goroutine 拥有自己的 ID 编号等

如何解决呢?要解决的话必然涉及到通信问题,可以将 recover() 得到的错误信息通过 channel 发送到全局的的捕获中心,记录下来。

简易代码示例如下:

1
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"fmt"
	"time"
)

var notifier chan interface{}

// 启动通知监听,收到通知就记录下来
func startPanicRecording() {
	notifier = make(chan interface{})
	go func() {
		for {
			select {
			case r := <-notifier:
				// 记录下来
				fmt.Println("got panic:", r)
			}
		}
	}()
}

// 对 goroutine 进行包装,增加 recover 
func Go(f func()) {
	go func() {
		defer func() {
			if r := recover(); r != nil {
				notifier <- r
			}
		}()
		f()
	}()
}
func main() {
	startPanicRecording()

	Go(func() {
		m := make([]int, 0)
        // 下标越界,产生 panic
		fmt.Println(m[0])
	})
    // 避免主 goroutine 过快结束
	time.Sleep(time.Second)
}

这个方案并不完美,如果 GO(f func()) 的参数没有嵌套调用 GO(f func()) 仍然阻止不了 panic,而且对于下面这样的代码也恢复不了:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
	m := make(map[int]int)
	var wg sync.WaitGroup
    // 并发写 map,造成 fatal error
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(data int) {
			m[data] = data * data
			wg.Done()
		}(i)
	}
	wg.Wait()
}

在 Go 官方没有提供特别完美的方案之前,建议开发者写好稳健的代码,做好充分的测试,再在运维上增加容灾机制。

本文由作者按照 CC BY 4.0 进行授权