go Panic & Recover

Java基础

浏览数:283

2020-6-16

Panic

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

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。

由于panic会引起程序的崩溃,因此panic一般用于严重错误,如该错误的作用范围影响到整个系统的运行。勤奋的程序员认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。

defer

defer是一个面向编译器的声明,他会让编译器做两件事:

  • 编译器会将defer声明编译为runtime.deferproc(fn),这样运行时,会调用runtime.deferproc,在deferproc中将所有defer挂到goroutine的defer链上;
  • 编译器会在函数return之前(注意,是return之前,而不是return xxx之前,后者不是一条原子指令),增加runtime.deferreturn调用;这样运行时,开始处理前面挂在defer链上的所有defer。

Recover

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。

不加区分的恢复所有的panic异常,不是可取的做法;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。

虽然把对panic的处理都集中在一个包下,有助于简化对复杂和不可以预料问题的处理,但作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回,而不是panic。同样的,你也不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的。

有时我们很难完全遵循规范,举个例子,net/http包中提供了一个web服务器,将收到的请求分发给用户提供的处理函数。很显然,我们不能因为某个处理函数引发的panic异常,杀掉整个进程;web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。

基于以上原因,安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常。

实例分析

千言万语不如看几个实例

实例1,在线源码调试:

package main

import (
    "fmt"
    "time"
)

func catchErr(num int) {
    // Try removing the following code of defer block
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("[recover]", err, num)
        }
    }()

    fmt.Println("goroutine", num)
    panic("panic occurred ...")
}

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("main recover: ", err)
        }
    }()

    for i := 0; i < 3; i++ {
        fmt.Println("main goroutine", i)
        go catchErr(i)
        time.Sleep(time.Second * 1)
    }

start:
    goto start

}

实例2,在线源码调试:

// Go program which illustrates 
// recover in a goroutine 
package main 

import ( 
    "fmt"
    "time"
) 

// For recovery 
func handlepanic() { 
    if a := recover(); a != nil { 
        fmt.Println("RECOVER", a) 
    } 
} 

/* Here, this panic is not 
handled by the recover 
function because of the 
recover function is not 
called in the same 
goroutine in which the 
panic occurs */

// Function 1 
func myfun1() { 

    defer handlepanic() 
    fmt.Println("Welcome to Function 1") 
    go myfun2() 
    time.Sleep(10 * time.Second) 
} 

// Function 2 
func myfun2() { 

    fmt.Println("Welcome to Function 2") 
    panic("Panicked!!") 
} 

// Main function 
func main() { 

    myfun1() 
    fmt.Println("Return successfully from the main function") 
} 

实例3,在线源码调试:

在此示例中,我们使用反射来检查接口变量列表是否具有与给定函数的参数相对应的类型。如果是这样,我们使用这些参数调用该函数以检查是否有紧急情况。

// Panics tells if function f panics with parameters p.
func Panics(f interface{}, p ...interface{}) bool {
    fv := reflect.ValueOf(f)
    ft := reflect.TypeOf(f)
    if ft.NumIn() != len(p) {
        panic("wrong argument count")
    }
    pv := make([]reflect.Value, len(p))
    for i, v := range p {
        if reflect.TypeOf(v) != ft.In(i) {
            panic("wrong argument type")
        }
        pv[i] = reflect.ValueOf(v)
    }
    return call(fv, pv)
}

func call(fv reflect.Value, pv []reflect.Value) (b bool) {
    defer func() {
        if err := recover(); err != nil {
            b = true
        }
    }()
    fv.Call(pv)
    return
}

总结

  1. panic() 执行后,后续语句不再执行,会先调用当前协程的 defer 链表。
  2. 如果某个 goroutine 的 defer 没有 recover,会终止整个程序(exit(2)),不仅仅是终止当前 goroutine 。
  3. 如发现 defer 函数包含 recover, 则会运行 recovery 函数,recovery 会跳转到 deferreturn 。
  4. panic 被 recover 后,会影响到当前函数中的后续语句的执行,但不影响当前 goroutine 的继续执行。
  5. recover() 的作用是捕获异常之后让程序正常往下执行而不会退出。
  6. recover() 必须写在defer语块中才能生效。
  7. recover() 的作用范围仅限于当前的所属 go routine。发生 panic 时只会执行当前协程中的defer函数,其它协程里面的 defer 不会执行。
  8. 如果要查找堆栈跟踪,请使用在 Debug 程序包下定义的 PrintStack 函数。
  9. 既然你要使用 panic,那为什么要 recover ?你的期望是什么?如果不希望 go die 为什么要用 panic ?
  10. 如果实在每个 panic 都想捕获,可以考虑把 panic 这样的事件通知给其他 goroutine 处理。

参考

Recover in Golang
Panics, stack traces and how to recover [best practice]
Defer, Panic, and Recover
Go语言panic/recover的实现
Go语言圣经

作者:维子