gopl-zh.github.com/ch5/ch5-09.md
Yue Chen 65ee267d20
Update ch5-09.md
修改源码和文件出处
2019-12-10 17:12:31 +08:00

5.4 KiB
Raw Blame History

5.9. Panic异常

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

一般而言当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.Must4.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机制中延迟函数的调用在释放堆栈信息之前。