Go 语言
错误处理
理解 panic

理解 panic

Go 的正常错误处理与异常处理直接是泾渭分明,这与其他主流编程语言使用结构化错误处理统一处理错误与异常是两种不同的理念。 Go 提供了 panic 专门用于处理异常,所以不要使用 panic 进行正常的错误处理。

panic 是一个 Go 内置函数,它用来停止当前常规控制流并启动 panicking 过程。当函数 F 调用 panic 函数时,函数 F 的执行停止, 函数 F 中已进行了求值的 defer 函数都讲得到正常执行,然后函数 F 将控制权返给其调用者。对于函数 F 的调用者而言,函数 F 之后的行为就如同调用者的函数是 panic 一样, 该 panicking 过程将继续在栈上进行下去,直到当前 goroutine 中的所有函数都返回为止,此时程序将崩溃退出。

panic 可以通过直接调用 panic 函数来引发,它们也可能是由运行时错误引起,例如越界数组访问。

panic 应用

有三种应用场景:

  • 充当断言角色,提示潜在 bug
  • 用于简化错误处理控制结构
  • 使用 recover 捕获 panic,防止 goroutine 意外退出

理解 panic 输出

下面是某个程序发生 panic 时真实输出的异常信息摘录:

panic: runtime error: index out of range
 
goroutine 1 [running]:
main.main()
    /Users/Chakhsu/Code/Go/src/github.com/chakhsu/go-tour/panic.go:8 +0x39
exit status 2

在 Go 1.11 及之后的版本中, Go 编译器得到更深入的优化,很多简单的函数和方法会被自动内联。函数一旦内联化,我们就无法在栈追踪信息中看到栈帧信息了,正如上面代码所示。

要想看到完整的栈追踪信息,我们需要使用 -gcflags="-1" 来告诉编译器不要执行内联优化。

panic: runtime error: index out of range
 
goroutine 1 [running]:
main.main()
    /Users/Chakhsu/Code/Go/src/github.com/chakhsu/go-tour/panic.go:8 +0x39
    /Users/Chakhsu/Code/Go/src/github.com/chakhsu/go-tour/panic.go:8
    /usr/local/go/src/runtime/proc.go:201
    /usr/local/go/src/runtime/asm_amd64.s:1337
exit status 2

对于 panic 导致的程序崩溃,我们首先要检查位于栈顶的栈追踪信息,并定位到直接引发 panic 的那一行代码。

多数情况下,通过这行代码即可直接揪出导致问题的元凶。如果没能做到,接下来,我们将继续调查 panic 输出的函数调用栈中参数是否正确。

关于发生 panic 后输出的栈追踪信息的识别,总体可遵循以下几点要数:

  • 栈追踪信息中的每个函数、方法后面的参数数值的个数与函数、方法原型的参数个数不是一一对应的。
  • 栈追踪信息中的每个函数、方法后面的参数数值是按照函数、方法原型参数列表中从左到右的参数类型的内存布局逐一展开的。
  • 如果是方法,则第一个参数是 receiver 自身。如果 receiver 是指针类型,则第一个参数是指针地址,否则栈追踪信息会按照其内存布局输出。
  • 函数、方法返回值放在栈追踪信息的参数数值列表后面:如果有多个返回值,,则同样按从左到右的顺序,按照返回值类型的内存布局输出。
  • 指针类型参数:占用栈追踪信息的参数数值的列表的一个位置:数值表示指针值,也是指针指向的对象的地址。
  • string 类型参数:由于 string 在内存中由两个字表示,第一个字是数据指针,第二个字是 string 长度,所以栈追踪信息的参数数值列表占两个位置。
  • slice 类型参数:由于 slice 在内存中由三个字表示,第一个字是数据指针,第二个字是 slice 长度,第三个字是 slice 容量,所以栈追踪信息的参数数值列表占三个位置。
  • 内建整型(int、rune、byte):由于按字逐个输出,对于类型长度不足一个字的参数,会进行合并处理。例如一个函数有5个 int16 类型的参数,那么在栈追踪信息中会占用两个位置,第一个位置是前4个参数的合同,第二个位置则是第5个参数。
  • struct 类型参数:会按照 struct 中的字段的内存布局顺序在栈追踪信息中展开。
  • interface 类型参数:由于 interface 类型在内存中由两部分组成(一部分是接口类型的参数指针,另外一部分是接口值的参数指针),因此在栈追踪信息中会占用两个位置。
  • 栈追踪输出的信息是在函数调用过程中的快照信息,因此一些输出数值虽然看似不合理,但由于其并不是最终值,问题也不一定发生在它们身上,比如返回值参数。