Post

panic与recover

panic

数据结构

1
2
3
4
5
6
7
type _panic struct {
    argp      unsafe.Pointer
    arg       interface{}    // panic 的参数
    link      *_panic        // 链接下一个 panic 结构体
    recovered bool           // 是否恢复,到此为止?
    aborted   bool           // the panic was aborted
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// panic函数
// runtime/panic.go
func gopanic(e interface{}) {
    // 在栈上分配一个 _panic 结构体
    var p _panic
    // 把当前最新的 _panic 挂到链表最前面
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    
    for {
        // 取出当前最近的 defer 函数;
        d := gp._defer
        if d == nil {
            // 如果没有 defer ,那就没有 recover 的时机,只能跳到循环外,退出进程了;
            break
        }

        // 进到这个逻辑,那说明了之前是有 panic 了,现在又有 panic 发生,这里一定处于递归之中;
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
            }
            // 把这个 defer 从链表中摘掉;
            gp._defer = d.link
            freedefer(d)
            continue
        }

        // 标记 _defer 为 started = true (panic 递归的时候有用)
        d.started = true
        // 记录当前 _defer 对应的 panic
        d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

        // 执行 defer 函数
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))

        // defer 执行完成,把这个 defer 从链表里摘掉;
        gp._defer = d.link
        
        // 取出 pc,sp 寄存器的值;
        pc := d.pc
        sp := unsafe.Pointer(d.sp)
        // 如果 _panic 被设置成恢复,那么到此为止;
        if p.recovered {
            // 摘掉当前的 _panic
            gp._panic = p.link
            // 如果前面还有 panic,并且是标记了 aborted 的,那么也摘掉;
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            // panic 的流程到此为止,恢复到业务函数堆栈上执行代码;
            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            // 注意:恢复的时候 panic 函数将从此处跳出,本 gopanic 调用结束,后面的代码永远都不会执行。
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    // 打印错误信息和堆栈,并且退出进程;
    preprintpanics(gp._panic)
    fatalpanic(gp._panic) // should not return
    *(*int)(nil) = 0      // not reached
}

panic 究竟是啥?是一个结构体?还是一个函数?

  • 背后执行gopanic函数

为什么 panic 会让 Go 进程退出的 ?

  • 因为gopanic函数调用了exit(2)

    为什么 recover 一定要放在 defer 里面才生效?

  • 因为gopanic函数执行会从当前的挂载的_defer链表取出defer延迟函数执行

    为什么 recover 已经放在 defer 里面,但是进程还是没有恢复?

  • 嵌套panic导致
  • defer只对当前的goroutine有效

为什么 panic 之后,还能再 panic ?有啥影响?

  • defer嵌套panic

总结

  • panic会新建一个_panic对象,放在链表表头。通过_panic.link下一个panic对象

  • panic嵌套,从链表尾部向上递归打印,如

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
      func main() {
      	defer func() { // 延迟函数
      		panic("panic again")
      	}()
      	panic("first")
      }
          
      // panic: first
      //       panic: panic again
          
      

参考

  • https://jishuin.proginn.com/p/763bfbd651e3

recover

源码

1
2
3
4
5
6
7
8
9
10
11
// runtime/panic.go
func recovery(gp *g) {
    // 取出栈寄存器和程序计数器的值
    sp := gp.sigcode0
    pc := gp.sigcode1
    // 重置 goroutine 的 pc,sp 寄存器;
    gp.sched.sp = sp
    gp.sched.pc = pc
    // 重新投入调度队列
    gogo(&gp.sched)
}
  • 1、_panic.recovered 字段被设置成 true
  • 2、修改pc、sp寄存器

总结

  • panic 的恢复,就是重置 pc 寄存器,直接跳转程序执行的指令,跳转到原本 defer 函数执行完该跳转的位置(deferreturn 执行),从 gopanic 函数中跳出,不再回来,自然就不会再 fatalpanic
This post is licensed under CC BY 4.0 by the author.