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?
}
go

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

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

虽然 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
}
go

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

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

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

ch5/defer1
Unresolved include directive in modules/ROOT/pages/ch5/ch5-09.adoc - include::example$/ch5/defer1/defer.go[]
go

上例中的运行输出如下:

Unresolved include directive in modules/ROOT/pages/ch5/ch5-09.adoc - include::example$/ch5/defer1/defer.go[]
bash

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

Unresolved include directive in modules/ROOT/pages/ch5/ch5-09.adoc - include::example$/ch5/defer1/defer.go[]
bash

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

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

Unresolved include directive in modules/ROOT/pages/ch5/ch5-09.adoc - include::example$/ch5/defer2/defer.go[]
go

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

Unresolved include directive in modules/ROOT/pages/ch5/ch5-09.adoc - include::example$/ch5/defer2/defer.go[]
bash

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