# 6.1 panic and recover

作为一个 gophper，我相信你对于 `panic` 和 `recover` 肯定不陌生，但是你有没有想过。当我们执行了这两条语句之后。底层到底发生了什么事呢？前几天和同事刚好聊到相关的话题，发现其实大家对这块理解还是比较模糊的。希望这篇文章能够从更深入的角度告诉你为什么，它到底做了什么事？

## 思考

### 一、为什么会中止运行

```
func main() {
    panic("EDDYCJY.")
}
```

输出结果：

```
$ go run main.go
panic: EDDYCJY.

goroutine 1 [running]:
main.main()
    /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39
exit status 2
```

请思考一下，为什么执行 `panic` 后会导致应用程序运行中止？（而不是单单说执行了 `panic` 所以就结束了这么含糊）

### 二、为什么不会中止运行

```
func main() {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("recover: %v", err)
        }
    }()

    panic("EDDYCJY.")
}
```

输出结果：

```
$ go run main.go 
2019/05/11 23:39:47 recover: EDDYCJY.
```

请思考一下，为什么加上 `defer` + `recover` 组合就可以保护应用程序？

### 三、不设置 defer 行不

上面问题二是 `defer` + `recover` 组合，那我去掉 `defer` 是不是也可以呢？如下：

```
func main() {
    if err := recover(); err != nil {
        log.Printf("recover: %v", err)
    }

    panic("EDDYCJY.")
}
```

输出结果：

```
$ go run main.go
panic: EDDYCJY.

goroutine 1 [running]:
main.main()
    /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:10 +0xa1
exit status 2
```

竟然不行，啊呀毕竟入门教程都写的 `defer` + `recover` 组合 “万能” 捕获。但是为什么呢。去掉 `defer` 后为什么就无法捕获了？

请思考一下，为什么需要设置 `defer` 后 `recover` 才能起作用？

同时你还需要仔细想想，我们设置 `defer` + `recover` 组合后就能无忧无虑了吗，各种 “乱” 写了吗？

### 四、为什么起个 goroutine 就不行

```
func main() {
    go func() {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("recover: %v", err)
            }
        }()
    }()

    panic("EDDYCJY.")
}
```

输出结果：

```
$ go run main.go 
panic: EDDYCJY.

goroutine 1 [running]:
main.main()
    /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:14 +0x51
exit status 2
```

请思考一下，为什么新起了一个 `Goroutine` 就无法捕获到异常了？到底发生了什么事...

## 源码

接下来我们将带着上述 4+1 个小思考题，开始对源码的剖析和分析，尝试从阅读源码中找到思考题的答案和更多为什么

### 数据结构

```
type _panic struct {
    argp      unsafe.Pointer
    arg       interface{} 
    link      *_panic 
    recovered bool
    aborted   bool 
}
```

在 `panic` 中是使用 `_panic` 作为其基础单元的，每执行一次 `panic` 语句，都会创建一个 `_panic`。它包含了一些基础的字段用于存储当前的 `panic` 调用情况，涉及的字段如下：

* argp：指向 `defer` 延迟调用的参数的指针
* arg：`panic` 的原因，也就是调用 `panic` 时传入的参数
* link：指向上一个调用的 `_panic`
* recovered：`panic` 是否已经被处理，也就是是否被 `recover`
* aborted：`panic` 是否被中止

另外通过查看 `link` 字段，可得知其是一个链表的数据结构，如下图：

![image](http://wx3.sinaimg.cn/large/006fVPCvly1g2muc73jp1j30hc099q2x.jpg)

### 恐慌 panic

```
func main() {
    panic("EDDYCJY.")
}
```

输出结果：

```
$ go run main.go
panic: EDDYCJY.

goroutine 1 [running]:
main.main()
    /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39
exit status 2
```

我们去反查一下 `panic` 处理具体逻辑的地方在哪，如下：

```
$ go tool compile -S main.go
"".main STEXT size=66 args=0x0 locals=0x18
    0x0000 00000 (main.go:23)    TEXT    "".main(SB), ABIInternal, $24-0
    0x0000 00000 (main.go:23)    MOVQ    (TLS), CX
    0x0009 00009 (main.go:23)    CMPQ    SP, 16(CX)
    ...
    0x002f 00047 (main.go:24)    PCDATA    $2, $0
    0x002f 00047 (main.go:24)    MOVQ    AX, 8(SP)
    0x0034 00052 (main.go:24)    CALL    runtime.gopanic(SB)
```

显然汇编代码直指内部实现是 `runtime.gopanic`，我们一起来看看这个方法做了什么事，如下（省略了部分）：

```
func gopanic(e interface{}) {
    gp := getg()
    ...
    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

    for {
        d := gp._defer
        if d == nil {
            break
        }

        // defer...
        ...
        d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

        p.argp = unsafe.Pointer(getargp(0))
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
        p.argp = nil

        // recover...
        if p.recovered {
            ...
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    preprintpanics(gp._panic)

    fatalpanic(gp._panic) // should not return
    *(*int)(nil) = 0      // not reached
}
```

* 获取指向当前 `Goroutine` 的指针
* 初始化一个 `panic` 的基本单位 `_panic` 用作后续的操作
* 获取当前 `Goroutine` 上挂载的 `_defer`（数据结构也是链表）
* 若当前存在 `defer` 调用，则调用 `reflectcall` 方法去执行先前 `defer` 中延迟执行的代码，若在执行过程中需要运行 `recover` 将会调用 `gorecover` 方法
* 结束前，使用 `preprintpanics` 方法打印出所涉及的 `panic` 消息
* 最后调用 `fatalpanic` 中止应用程序，实际是执行 `exit(2)` 进行最终退出行为的

通过对上述代码的执行分析，可得知 `panic` 方法实际上就是处理当前 `Goroutine(g)` 上所挂载的 `._panic` 链表（所以无法对其他 `Goroutine` 的异常事件响应），然后对其所属的 `defer` 链表和 `recover` 进行检测并处理，最后调用退出命令中止应用程序

### 无法恢复的恐慌 fatalpanic

```
func fatalpanic(msgs *_panic) {
    pc := getcallerpc()
    sp := getcallersp()
    gp := getg()
    var docrash bool

    systemstack(func() {
        if startpanic_m() && msgs != nil {
            ...
            printpanics(msgs)
        }

        docrash = dopanic_m(gp, pc, sp)
    })

    systemstack(func() {
        exit(2)
    })

    *(*int)(nil) = 0
}
```

我们看到在异常处理的最后会执行该方法，似乎它承担了所有收尾工作。实际呢，它是在最后对程序执行 `exit` 指令来达到中止运行的作用，但在结束前它会通过 `printpanics` 递归输出所有的异常消息及参数。代码如下：

```
func printpanics(p *_panic) {
    if p.link != nil {
        printpanics(p.link)
        print("\t")
    }
    print("panic: ")
    printany(p.arg)
    if p.recovered {
        print(" [recovered]")
    }
    print("\n")
}
```

所以不要以为所有的异常都能够被 `recover` 到，实际上像 `fatal error` 和 `runtime.throw` 都是无法被 `recover` 到的，甚至是 oom 也是直接中止程序的，也有反手就给你来个 `exit(2)` 教做人。因此在写代码时你应该要相对注意些，“恐慌” 是存在无法恢复的场景的

### 恢复 recover

```
func main() {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("recover: %v", err)
        }
    }()

    panic("EDDYCJY.")
}
```

输出结果：

```
$ go run main.go 
2019/05/11 23:39:47 recover: EDDYCJY.
```

和预期一致，成功捕获到了异常。但是 `recover` 是怎么恢复 `panic` 的呢？再看看汇编代码，如下：

```
$ go tool compile -S main.go
"".main STEXT size=110 args=0x0 locals=0x18
    0x0000 00000 (main.go:5)    TEXT    "".main(SB), ABIInternal, $24-0
    ...
    0x0024 00036 (main.go:6)    LEAQ    "".main.func1·f(SB), AX
    0x002b 00043 (main.go:6)    PCDATA    $2, $0
    0x002b 00043 (main.go:6)    MOVQ    AX, 8(SP)
    0x0030 00048 (main.go:6)    CALL    runtime.deferproc(SB)
    ...
    0x0050 00080 (main.go:12)    CALL    runtime.gopanic(SB)
    0x0055 00085 (main.go:12)    UNDEF
    0x0057 00087 (main.go:6)    XCHGL    AX, AX
    0x0058 00088 (main.go:6)    CALL    runtime.deferreturn(SB)
    ...
    0x0022 00034 (main.go:7)    MOVQ    AX, (SP)
    0x0026 00038 (main.go:7)    CALL    runtime.gorecover(SB)
    0x002b 00043 (main.go:7)    PCDATA    $2, $1
    0x002b 00043 (main.go:7)    MOVQ    16(SP), AX
    0x0030 00048 (main.go:7)    MOVQ    8(SP), CX
    ...
    0x0056 00086 (main.go:8)    LEAQ    go.string."recover: %v"(SB), AX
    ...
    0x0086 00134 (main.go:8)    CALL    log.Printf(SB)
    ...
```

通过分析底层调用，可得知主要是如下几个方法：

* runtime.deferproc
* runtime.gopanic
* runtime.deferreturn
* runtime.gorecover

在上小节中，我们讲述了简单的流程，`gopanic` 方法会调用当前 `Goroutine` 下的 `defer` 链表，若 `reflectcall` 执行中遇到 `recover` 就会调用 `gorecover` 进行处理，该方法代码如下：

```
func gorecover(argp uintptr) interface{} {
    gp := getg()
    p := gp._panic
    if p != nil && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}
```

这代码，看上去挺简单的，核心就是修改 `recovered` 字段。该字段是用于标识当前 `panic` 是否已经被 `recover` 处理。但是这和我们想象的并不一样啊，程序是怎么从 `panic` 流转回去的呢？是不是在核心方法里处理了呢？我们再看看 `gopanic` 的代码，如下：

```
func gopanic(e interface{}) {
    ...
    for {
        // defer...
        ...
        pc := d.pc
        sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
        freedefer(d)

        // recover...
        if p.recovered {
            atomic.Xadd(&runningPanicDefers, -1)

            gp._panic = p.link
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            if gp._panic == nil { 
                gp.sig = 0
            }

            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            mcall(recovery)
            throw("recovery failed") 
        }
    }
    ...
}
```

我们回到 `gopanic` 方法中再仔细看看，发现实际上是包含对 `recover` 流转的处理代码的。恢复流程如下：

* 判断当前 `_panic` 中的 `recover` 是否已标注为处理
* 从 `_panic` 链表中删除已标注中止的 `panic` 事件，也就是删除已经被恢复的 `panic` 事件
* 将相关需要恢复的栈帧信息传递给 `recovery` 方法的 `gp` 参数（每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量）
* 执行 `recovery` 进行恢复动作

从流程来看，最核心的是 `recovery` 方法。它承担了异常流转控制的职责。代码如下：

```
func recovery(gp *g) {
    sp := gp.sigcode0
    pc := gp.sigcode1

    if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
        print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
        throw("bad recovery")
    }

    gp.sched.sp = sp
    gp.sched.pc = pc
    gp.sched.lr = 0
    gp.sched.ret = 1
    gogo(&gp.sched)
}
```

粗略一看，似乎就是很简单的设置了一些值？但实际上设置的是编译器中伪寄存器的值，常常被用于维护上下文等。在这里我们需要结合 `gopanic` 方法一同观察 `recovery` 方法。它所使用的栈指针 `sp` 和程序计数器 `pc` 是由当前 `defer` 在调用流程中的 `deferproc` 传递下来的，因此实际上最后是通过 `gogo` 方法跳回了 `deferproc` 方法。另外我们注意到：

```
gp.sched.ret = 1
```

在底层中程序将 `gp.sched.ret` 设置为了 1，也就是**没有实际调用** `deferproc` 方法，直接修改了其返回值。意味着默认它已经处理完成。直接转移到 `deferproc` 方法的下一条指令去。至此为止，异常状态的流转控制就已经结束了。接下来就是继续走 `defer` 的流程了

为了验证这个想法，我们可以看一下核心的跳转方法 `gogo` ，代码如下：

```
// void gogo(Gobuf*)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB),NOSPLIT,$8-4
    MOVW    buf+0(FP), R1
    MOVW    gobuf_g(R1), R0
    BL    setg<>(SB)

    MOVW    gobuf_sp(R1), R13    // restore SP==R13
    MOVW    gobuf_lr(R1), LR
    MOVW    gobuf_ret(R1), R0
    MOVW    gobuf_ctxt(R1), R7
    MOVW    $0, R11
    MOVW    R11, gobuf_sp(R1)    // clear to help garbage collector
    MOVW    R11, gobuf_ret(R1)
    MOVW    R11, gobuf_lr(R1)
    MOVW    R11, gobuf_ctxt(R1)
    MOVW    gobuf_pc(R1), R11
    CMP    R11, R11 // set condition codes for == test, needed by stack split
    B    (R11)
```

通过查看代码可得知其主要作用是从 `Gobuf` 恢复状态。简单来讲就是将寄存器的值修改为对应 `Goroutine(g)` 的值，而在文中讲了很多次的 `Gobuf`，如下：

```
type gobuf struct {
    sp   uintptr
    pc   uintptr
    g    guintptr
    ctxt unsafe.Pointer
    ret  sys.Uintreg
    lr   uintptr
    bp   uintptr
}
```

讲道理，其实它存储的就是 `Goroutine` 切换上下文时所需要的一些东西

## 拓展

```
const(
    OPANIC       // panic(Left)
    ORECOVER     // recover()
    ...
)
...
func walkexpr(n *Node, init *Nodes) *Node {
    ...
    switch n.Op {
    default:
        Dump("walk", n)
        Fatalf("walkexpr: switch 1 unknown op %+S", n)

    case ONONAME, OINDREGSP, OEMPTY, OGETG:
    case OTYPE, ONAME, OLITERAL:
        ...
    case OPANIC:
        n = mkcall("gopanic", nil, init, n.Left)

    case ORECOVER:
        n = mkcall("gorecover", n.Type, init, nod(OADDR, nodfp, nil))
    ...
}
```

实际上在调用 `panic` 和 `recover` 关键字时，是在编译阶段先转换为相应的 OPCODE 后，再由编译器转换为对应的运行时方法。并不是你所想像那样一步到位，有兴趣的小伙伴可以研究一下

## 总结

本文主要针对 `panic` 和 `recover` 关键字进行了深入源码的剖析，而开头的 4+1 个思考题，就是希望您能够带着疑问去学习，达到事半功倍的功效

另外本文和 `defer` 有一定的关联性，因此需要有一定的基础知识。若刚刚看的时候这部分不理解，学习后可以再读一遍加深印象

在最后，现在的你可以回答这几个思考题了吗？说出来了才是真的懂 ：）


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://eddycjy.gitbook.io/golang/di-6-ke-chang-yong-guan-jian-zi/panic-and-recover.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
