recover 并不是无懈可击的

曾经天真地认为只要严格遵守有 go 必有 recover 就能保证程序永不宕机。直到有人对开源库有了类似下面这样的误用:

package main

func main() {
	var a = map[int]int{}
	defer func() {
		if err := recover(); err != nil {
			println("oh no")
		}
	}()

	go func() {
		defer func() {
			if err := recover(); err != nil {
				println("oh no")
			}
		}()
		for {
			a[1] = 1
		}
	}()

	for {
		a[1] = 1
	}
}

当然了,实际的场景不会有这么弱智,这样写主要是为了方便复现。

go 在 1.6 增加了 map 并发读写时,直接让程序崩溃的新 feature,崩溃是用 panic,然而这种 panic 通过 recover 是捕获不到的。因为官方认为这是一种不可恢复的错误。程序继续运行结果不可知了吧。

具体参考这里

在 go 1.9 官方引入了 sync.Map,然而这个新玩具就只是为了解决官方自己在 grpc 中遇到的性能瓶颈,即由于读写锁的 cpu cache contention 导致的性能下降问题。而小版本升级中也没有为这个 sync.Map 设计特权语法,store 和 load 接口均需要操作 interface 类型,实际上虽然性能好了一点,但如果你真的要用,那结果就是满天飞的 interface。

扯远了。说回 panic。

你可能以为在 go 里,map 的并发读写是唯一一种捕获不到,那么我们再来看一个 demo。

package main

import "reflect"

func main() {
	defer func() {
		if err := recover(); err != nil {
			println("panic")
		}
	}()
	fv := reflect.ValueOf(func(int) struct{} { return struct{}{} })
	args := []reflect.Value{reflect.ValueOf(0)}

	for {
		fv.Call(args)
	}
}

你手边有任意的环境都可以尝试一下,实际上这段代码会触发 golang 自己的 bug(汗。。

直到 1.9.2,这个 bug 才修复。触发条件就像 demo 里一样,高并发下调用 reflect.Call,一调一个准。且也无法用 recover 来恢复。

结论:go 服务不用 supervisor 又不加监控就等着被开除吧