> For the complete documentation index, see [llms.txt](https://sunwenfei.gitbook.io/sunwenfei/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/cuo-wu/panic-he-recover.md).

# panic和recover

`panic`和`recover`翻译成中文是`恐慌`和`恢复`，我个人感觉比较别扭，还是直接用英文吧。

#### 什么是`panic`

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

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

#### 什么场景使用`panic`

我们通常应该使用`error`来处理异常情况，除非程序无法继续运行下去了再使用`panic`和`recover`。

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

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

   &#x20;比如我们要实现web服务器，如果端口绑定失败，就可以生成一个`panic`。
2. 编程错误。

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

#### `panic`举例

Golang中`panic`函数的签名如下：

```go
func panic(interfce{})
```

传给`panic`函数的入参会在程序终止时打印出来。我们通过例子来理解下。

```go
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`中判断了`firstName`和`lastName`是否是`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`调用语句。

```go
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`

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

```go
func recover() interface{}
```

下面我们通过例子看下`recover`是如何将`panic`的程序恢复到正常执行的。

```go
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`只能恢复当前`goroutine`的`panic`

`recover`只能恢复当前`goroutine`的`panic`，不能恢复其他`gorutine`的`panic`。

```go
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
```

这时`b`和`a`的执行都位于主`goroutine`，因此可以正常恢复。

#### 运行时`panic`

在Golang中，有一些运行时错误也会引发`panic`，比如数组访问越界。这种情况相当于我们手动调用`panic`，并传入[runtime.Error](https://golang.org/src/runtime/error.go?s=267:503#L1)类型的参数。接口`runtime.Error`定义如下：

```go
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`。

```go
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`来恢复。

```go
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](https://golang.org/pkg/runtime/debug/)包的[PrintStack](https://golang.org/pkg/runtime/debug/#PrintStack)函数来手动打印调用栈。

```go
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
```

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