panic和recover

panicrecover翻译成中文是恐慌恢复,我个人感觉比较别扭,还是直接用英文吧。

什么是panic

Golang中处理错误的常规方式是使用前面我们讨论的error。但是有一些比较特殊的场景,发生错误后我们的程序就无法运行下去了。这时我们通过panic来终止程序的执行。当一个函数中发生panic时,该函数停止继续执行,然后该函数中被defer的函数执行,虽然程序执行控制权转移给外层函数。该过程会冒泡直到goroutine级别。然后打印panic信息和调用栈并终止整个程序的执行。现在看起来可能有点晦涩难懂,后面我们通过几个简单的例子看下应该会清晰很多。

如果发生了panic,我们可以通过recover来恢复程序的执行,panicrecover的关系就像其他编程语言中的try throw catch

什么场景使用panic

我们通常应该使用error来处理异常情况,除非程序无法继续运行下去了再使用panicrecover

以下两种情况比较适合使用panic

  1. 有异常情况导致程序无法继续运行下去。

    比如我们要实现web服务器,如果端口绑定失败,就可以生成一个panic

  2. 编程错误。

    比如我们期望一个函数的入参为指针,如果传进来的是nil,那么就是编程实现逻辑错误,这时可以生成一个panic

panic举例

Golang中panic函数的签名如下:

func panic(interfce{})

传给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")
}

该程序用来打印一个人的姓名的全称。函数fullName中判断了firstNamelastName是否是nil。如果是nil就调用了panic方法,调用时传入了一个字符串用来表示错误信息。该错误信息会在程序终止时打印,该程序执行结果如下:

panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0xc000074f68, 0x0)

/Users/goWorkspace/src/test/main.go:10 +0x145
main.main()

/Users/goWorkspace/src/test/main.go:18 +0x4d
exit status 2

我们分析下这个打印结果来探究下panic的工作原理。我们调用fullName时第二个参数传的是nil,因此fullName内执行了panic语句。Golang程序执行过程中如果遇到panic就会终止执行,并打印传给panic的参数和调用栈。由于panic是发生在fullName函数内,因此一次打印了main.fullName(0xc000074f68, 0x0)main.main()

defer + panic

如果一个函数内发生了panic,那么会先执行defer调用的函数,然后再转移到外层函数。该过程会不断向外冒泡直到goroutine级别。然后打印panic信息和调用栈。 前面的例子中没有defer函数调用。如果有defer函数调用,那么会先执行defer调用的函数,然后程序执行控制权再转移到外层函数。下面我们增加个defer调用语句。

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

goroutine 1 [running]:
main.fullName(0xc000074f58, 0x0)
        /Users/work/goWorkspace/src/test/main.go:11 +0x1d1
main.main()
        /Users/work/goWorkspace/src/test/main.go:20 +0xa5
exit status 2

这段程序中fullName发生panic时先执行被defer的函数调用,打印了deferred call in fullName。然后程序执行控制权转移给外层函数,接着执行外层函数被defer的函数调用,打印了deferred call in main。此时已到goroutine级别,因此不会再接着向外冒泡了。接着打印了panic信息以及调用栈。

recover

recoverpanic一样,也是Golan内置函数。recover一般在被defer的函数内部调用,用来将程序恢复到正常。panicrecover跟其他编程需要中的try catch非常像,一个用来抛错误,一个用来处理错误并恢复程序的正常执行。recover函数签名如下:

func recover() interface{}

下面我们通过例子看下recover是如何将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")
}

这里我们在被defer的函数中调用了recover来将panic恢复到正常。注意recover()函数的调用结果即为panic的参数。这里我们拿到panic的参数后只是进行了打印。该程序执行结果如下:

recovered from  runtime error: last name cannot be nil
returned normally from main
deferred call in main

recover只能恢复当前goroutinepanic

recover只能恢复当前goroutinepanic,不能恢复其他gorutinepanic

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")
}

这里在goroutine b中发生了panic,但是函数a的执行位于主goroutine,跟b不是一个,因此函数a中的recover无法恢复b中的panic。该程序执行结果如下:

Inside A
Inside B
panic: oh! B panicked

goroutine 5 [running]:
main.b()
        /Users/work/goWorkspace/src/test/main.go:23 +0x79
created by main.a
        /Users/work/goWorkspace/src/test/main.go:17 +0x95
exit status 2

如果b函数调用不是在一个新的goroutine,比如我们将go b()改为b(),则执行结果如下:

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

这时ba的执行都位于主goroutine,因此可以正常恢复。

运行时panic

在Golang中,有一些运行时错误也会引发panic,比如数组访问越界。这种情况相当于我们手动调用panic,并传入runtime.Error类型的参数。接口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接口。我们通过例子来看下运行时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。该程序输出如下:

panic: runtime error: index out of range

goroutine 1 [running]:
main.a()
        /Users/work/goWorkspace/src/test/main.go:7 +0x11
main.main()
        /Users/work/goWorkspace/src/test/main.go:11 +0x22
exit status 2

运行时panic也可以通过recover来恢复。

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")
}

这里我们在被defer的函数r中将panic的程序恢复正常。该程序输出为:

Recovered runtime error: index out of range
normally returned from main

recover后打印调用栈

从前面的几个例子我们会发现,程序recover后调用栈就不会被自动打印了。不过我们可以借助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")
}

这里我们通过debug.PrintStack()来手动打印了调用栈。该程序输出如下:

Recovered runtime error: index out of range
goroutine 1 [running]:
runtime/debug.Stack(0xc00007a008, 0xc00006ee08, 0x2)
        /usr/local/go/src/runtime/debug/stack.go:24 +0xa7
runtime/debug.PrintStack()
        /usr/local/go/src/runtime/debug/stack.go:16 +0x22
main.r()
        /Users/work/goWorkspace/src/test/main.go:11 +0x9c
panic(0x10a9bc0, 0x115d560)
        /usr/local/go/src/runtime/panic.go:513 +0x1b9
main.a()
        /Users/work/goWorkspace/src/test/main.go:18 +0x3e
main.main()
        /Users/work/goWorkspace/src/test/main.go:23 +0x22
normally returned from main

这里我们先恢复了程序的正常,然后紧接着手动打印了调用栈。

Last updated