5.9. Panic异常

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起panic异常。

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。

不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径:

switch s := suit(drawCard()); s {
case "Spades":                                // ...
case "Hearts":                                // ...
case "Diamonds":                              // ...
case "Clubs":                                 // ...
default:
	panic(fmt.Sprintf("invalid suit %q", s)) // Joker?
}

断言函数必须满足的前置条件是明智的做法,但这很容易被滥用。除非你能提供更多的错误信息,或者能更快速的发现错误,否则不需要使用断言,编译器在运行时会帮你检查代码。

func Reset(x *Buffer) {
	if x == nil {
		panic("x is nil") // unnecessary!
	}
	x.elements = nil
}

虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。勤奋的程序员认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。

考虑regexp.Compile函数,该函数将正则表达式编译成有效的可匹配格式。当输入的正则表达式不合法时,该函数会返回一个错误。当调用者明确的知道正确的输入不会引起函数错误时,要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法,就如前面的断言一样:当调用者输入了不应该出现的输入时,触发panic异常。

在程序源码中,大多数正则表达式是字符串字面值(string literals),因此regexp包提供了包装函数regexp.MustCompile检查输入的合法性。

package regexp
func Compile(expr string) (*Regexp, error) { /* ... */ }
func MustCompile(expr string) *Regexp {
	re, err := Compile(expr)
	if err != nil {
		panic(err)
	}
	return re
}

包装函数使得调用者可以便捷的用一个编译后的正则表达式为包级别的变量赋值:

var httpSchemeRE = regexp.MustCompile(`^https?:`) //"http:" or "https:"

显然,MustCompile不能接收不合法的输入。函数名中的Must前缀是一种针对此类函数的命名约定,比如template.Must(4.6节)

func main() {
	f(3)
}
func f(x int) {
	fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
	defer fmt.Printf("defer %d\n", x)
	f(x - 1)
}

上例中的运行输出如下:

f(3)
f(2)
f(1)
defer 1
defer 2
defer 3

当f(0)被调用时,发生panic异常,之前被延迟执行的3个fmt.Printf被调用。程序中断执行后,panic信息和堆栈信息会被输出(下面是简化的输出):

panic: runtime error: integer divide by zero
main.f(0)
src/gopl.io/ch5/defer1/defer.go:14
main.f(1)
src/gopl.io/ch5/defer1/defer.go:16
main.f(2)
src/gopl.io/ch5/defer1/defer.go:16
main.f(3)
src/gopl.io/ch5/defer1/defer.go:16
main.main()
src/gopl.io/ch5/defer1/defer.go:10

我们在下一节将看到,如何使程序从panic异常中恢复,阻止程序的崩溃。

为了方便诊断问题,runtime包允许程序员输出堆栈信息。在下面的例子中,我们通过在main函数中延迟调用printStack输出堆栈信息。

gopl.io/ch5/defer2

func main() {
	defer printStack()
	f(3)
}
func printStack() {
	var buf [4096]byte
	n := runtime.Stack(buf[:], false)
	os.Stdout.Write(buf[:n])
}

printStack的简化输出如下(下面只是printStack的输出,不包括panic的日志信息):

goroutine 1 [running]:
main.printStack()
src/gopl.io/ch5/defer2/defer.go:20
main.f(0)
src/gopl.io/ch5/defer2/defer.go:27
main.f(1)
src/gopl.io/ch5/defer2/defer.go:29
main.f(2)
src/gopl.io/ch5/defer2/defer.go:29
main.f(3)
src/gopl.io/ch5/defer2/defer.go:29
main.main()
src/gopl.io/ch5/defer2/defer.go:15

将panic机制类比其他语言异常机制的读者可能会惊讶,runtime.Stack为何能输出已经被释放函数的信息?在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。