Go学习笔记14-panic和recover

什么是panic?

在Go中,对于程序中出现的大部分异常情况,错误就已经够用了。但在有些情况下,当程序发生异常时无法继续运行,那么可使用panic来终止程序。

当函数发生panic时,它会终止运行,在执行完所有延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出panic信息,接着打印出堆栈跟踪,最后程序终止。

何时应该使用panic?

通常,我们应该尽可能地使用错误,而不是使用panic和recover。只有当程序不能继续运行的时候,才使用panic和recover机制。

panic有两个合理的用例:

  • 发生了一个不能恢复的错误,此时程序不能继续运行。一个例子就是web服务器无法绑定所要求的端口。在这种情况下,就应该使用panic,因为如果不能绑定端口,接下来什么也做不了。
  • 发生了一个编程上的错误。假如我们有一个接收指针参数的方法,而其他人使用nil作为参数调用了它。在这种情况下,我们可以使用panic,因为这是一个编程错误:用nil参数调用了一个只能接收合法指针的方法。

示例

内建函数panic的签名:func panic(v interface{}),当程序终止时,会打印传入panic的参数。

package main

import "fmt"

func fullName(firstName *string, lastName *string) {
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

上述代码,首先会打印出传入panic函数的信息:panic: runtime error: last name cannot be nil,接着打印出堆栈跟踪。

发生panic时的defer

当函数发生panic时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出panic信息,接着打印出堆栈跟踪,最后程序终止。也就是说,如果有延迟函数,会先调用它,然后程序控制返回到函数调用方。

使用一个例子来说明:

package main

import "fmt"

func fullName(firstName *string, lastName *string) {
    defer fmt.Println("deferred call in fullName")
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}
/*
deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil
*/

recover

recover是一个内建函数func recover() interface{},用于重新获得panic协程的控制。只有在延迟函数内部调用recover才有用。在延迟函数内调用recover,可以取到panic的错误信息,并且停止panic续发事件,程序恢复正常。

package main

import "fmt"

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

func fullName(firstName *string, lastName *string) {
    defer recoverName()
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}
/*
recovered from  runtime error: last name cannot be nil
returned normally from main
deferred call in main
*/

panic、recover、goroutine

只有在相同的Go协程中调用recover才管用。recover不能恢复一个不同协程的panic,看例子:

package main

import (
    "fmt"
    "time"
)

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

func a() {
    defer recovery() // panic并不会恢复,因为是在b协程中发生panic
    fmt.Println("Inside A")
    go b()
    time.Sleep(time.Second)
}

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

func main() {
    a()
    fmt.Println("normally returned from main")
}
/*
Inside A
Inside B
panic: oh! B panicked
*/

运行时panic

运行时错误(如数组越界)也会导致panic。这等价于调用了内置函数panic。

package main

import "fmt"

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

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

func main() {
    a()
    fmt.Println("normally returned from main")
}
/*
recovered: runtime error: index out of range
normally returned from main
*/

恢复后获得堆栈跟踪

当恢复panic时,就释放了它的堆栈跟踪。使用Debug包中的PrintStack函数可以打印出堆栈跟踪。

package main

import (
    "fmt"
    "runtime/debug"
)

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

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

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

reference:

https://studygolang.com/articles/12785