go: panic与recover

panic是go语言在运行时发生的错误,如数组访问越界、空指针引用等,这些运行时错误会引起程序崩溃。这并不是我们想看到的,程序崩溃可能造成体验停止、服务中断,就像没有人希望在马桶冲水冲到一半时坏掉。
go: panic与recover_第1张图片

什么是panic?

我们一般用error来处理go语言中遇到的错误。Errors能满足我们大部分时候的开发需求

但有情况下,程序在发生异常后无法继续运行下去,这时我们用panic终止程序。

当一个函数发生了panic,他所执行的内容会立刻停止,并且defer过的函数紧接着被执行,最后return回该函数的调用者。这个过程会一直进行直到整个 goroutines的函数都返回了,此时程序将打印panic信息,然后打印调用栈,终止。

稍后的例子会清晰地描述这一过程

什么时候应该使用panic

有一点应该明白的是,我们应该避免程序panic或recover,尽可能的使用error。仅在程序无法继续执行的情况下才应使用panic和recover机制。

有两种情况需要使用panic

  1. 一个程序遇到后不可继续执行的错误。
    举个例子,web server无法绑定请求端口。这种情况下不panic就什么也执行不下去

  2. 写程序出错
    假设有一个接受指针为参数的方法,有人用nil作为参数去调用它,在这种情况下会产生panic。

Panic实例

内置panic函数签名:

func panic(interface{})  

传入panic的参数会被打印到终端。

上代码

package main

import (
	"fmt"
)

func div(a,b int) int {
	return a/b
}

func main() {
	result:=div(3, 0)
	fmt.Println(result)
	fmt.Println("returned normally from main")
}

输出

panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.div(…)
/tmp/sandbox624380051/prog.go:8
main.main()
/tmp/sandbox624380051/prog.go:12 +0x20

上面的例子在运行到12行的时候出现了除数为0的情况,于是调用panic后直接执行最外层函数也即是main函数的rerturn,所以能看到并没有执行第14行的代码就退出了,并打印了跟踪栈。

在整个goroutine中,能看到/tmp/sandbox624380051/prog.go:8
首先在第8行触发了panic,下面是/tmp/sandbox624380051/prog.go:12,说明是在这里调用了上面的代码导致panic。到此,已经没有外层的函数调用,栈跟踪打印完毕。

在panic时defer

让我们看一下panic都做了什么,当一个函数发生了panic,他便立刻停止,接着任何被defer执行的函数开始执行然后return回调用该函数的调用者。这一过程会一直持续到所有goroutine都return了为止。此时程序打印panic消息,然后打印跟踪栈,最后终止。

再上一个例子中我们没有使用defer。这次我们做一些修改,使用defer语句

package main

import (
	"fmt"
)

func div(a,b int) int {
    	defer fmt.Println("deferred call in div")
	return a/b
}

func main() {
    defer fmt.Println("deferred call in main")
	result:=div(3, 0)
	fmt.Println(result)
	fmt.Println("returned normally from main")
}

输出

deferred call in div
deferred call in main
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.div(0x3, 0x0, 0x0, 0x104560)
/tmp/sandbox664019576/prog.go:9 +0x140
main.main()
/tmp/sandbox664019576/prog.go:14 +0xa0
上面的例子只在第8、13行添加defer执行函数。在程序panic时,首先执行当前函数中的defer,然后返回到该函数的调用者中再执行defer,以此类推直到顶级调用者为止。

panic,recover和goroutine

recover仅当在同一goroutine时才有效,无法recover其他goroutine发生的panic,下面举个例子

package main

import (  
    "fmt"
    "time"
)

func recovery() {  
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}

func a() {  
    defer recovery()
    fmt.Println("Inside A")
    go b()
    time.Sleep(1 * time.Second)
}

func b() {  
    fmt.Println("Inside B")
    panic("oh! B panicked")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}

在上面的例子中,函数b() 在第23行发生panic。函数a() 调用被defer的函数recovery() (捕获panic的作用),函数b() 在另一个goroutine中17行被调用,下一行的sleep只是为了保证程序不会在b() 还没执行完之前就退出。
你觉得会输出什么内容?panic会被捕获到吗?不会的,因为recover函数在不同的goroutine中,而触发panic的函数b() 在另外一个goroutine中,因此无法捕获panic

输出

Inside A
Inside B
panic: oh! B panicked

goroutine 6 [running]:
main.b()
/tmp/sandbox238905465/prog.go:23 +0xa0
created by main.a
/tmp/sandbox238905465/prog.go:17 +0xc0

从结果可以看到recover并未生效

如果函数b()在同一个goroutine中调用,则panic能被捕获到。
将上面的代码17行go b()改为b(),就能保证实在同一goroutine发生,recover可以生效,运行后会输出

Inside A
Inside B
recovered: oh! B panicked
normally returned from main

runtime panic

panic也可能是因为运行时引起的,例如数组越界访问,等于一个接口类型的 runtime.Error调用了panic,runtime.Error的接口定义如下

type Error interface {  
    error
    // RuntimeError is a no-op function but
    // serves to distinguish types that are run time
    // errors from ordinary errors: a type is a
    // run time error if it has a RuntimeError method.
    RuntimeError()
}

runtime.Error实现了内置error接口

我们写一个人为runtime panic的例子

package main

import (  
    "fmt"
)

func a() {  
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}
func main() {  
    a()
    fmt.Println("normally returned from main")
}

上面的例子中我们试图访问一个不存在的索引n[3],会导致崩溃。输出如下

panic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.a()
/tmp/sandbox935148947/prog.go:9 +0x20
main.main()
/tmp/sandbox935148947/prog.go:13 +0x20

当然了,这个情况也是可以被捕获到的,修改下代码

package main

import (  
    "fmt"
)

func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
    }
}

func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}

从输出中可以看到panic被recover了

Recovered runtime error: index out of range [3] with length 3
normally returned from main

recover后获取跟踪栈

我们recover了panic,但丢失有关panic的堆栈跟踪信息。

Debug包的PrintStack函数可以打印跟踪栈信息

package main

import (  
    "fmt"
    "runtime/debug"
)

func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
        debug.PrintStack()
    }
}

func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}

上面的代码11行debug.PrintStack()打印了跟踪栈

Recovered runtime error: index out of range [3] with length 3
goroutine 1 [running]:
runtime/debug.Stack(0x3e, 0x0, 0x0, 0x54b3)
/usr/local/go/src/runtime/debug/stack.go:24 +0xc0
runtime/debug.PrintStack()
/usr/local/go/src/runtime/debug/stack.go:16 +0x20
main.r()
/tmp/sandbox553506768/prog.go:11 +0xe0
panic(0x10cc60, 0x40e020)
/usr/local/go/src/runtime/panic.go:679 +0x240
main.a()
/tmp/sandbox553506768/prog.go:18 +0x60
main.main()
/tmp/sandbox553506768/prog.go:23 +0x20
normally returned from main

从输出可以了解到首先recover了runtime panic,接着打印了跟踪栈,在recover以后,打印了normally returned from main正常结束了函数。

快速回顾

  • 什么是panic?
  • 什么时候应该使用panic?
  • panic实例
  • defer panic实例
  • recover
  • panic,recover和goroutines
  • runtime panic
  • recover后获取跟踪栈信息

你可能感兴趣的:(go)