go defer / panic处理

栏目: IT技术 · 发布时间: 5年前

内容简介:作为一个 gophper,我相信你对于原文地址:输出结果:

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

原文地址: 深入理解 Go panic and 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.

请思考一下,为什么加上 deferrecover 组合就可以保护应用程序?

三、不设置 defer 行不

上面问题二是 deferrecover 组合,那我去掉  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

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

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

同时你还需要仔细想想,我们设置 deferrecover 组合后就能无忧无虑了吗,各种 “乱” 写了吗?

四、为什么起个 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 字段,可得知其是一个链表的数据结构,如下图:

go defer / panic处理

恐慌 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 , 
                                     // note: 这里是ljmp/gogo,是不存在返回的
                                     // ljmp对栈的操作有两种,
                                     // 一种是在现有栈往上jmp,一般是在相同G里做的,比如这里的recover。 
                                     // 一种是jmp到完全不相干的栈,一般是发生了G切换,跳到了另一个G的栈。当然极少也可能是从同个G的多个栈之间跳。
        }
    }

    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 有一定的关联性,因此需要有一定的基础知识。若刚刚看的时候这部分不理解,学习后可以再读一遍加深印象

在最后,现在的你可以回答这几个思考题了吗?说出来了才是真的懂 :)


以上所述就是小编给大家介绍的《go defer / panic处理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Unity 3D游戏开发(第2版)

Unity 3D游戏开发(第2版)

宣雨松 / 人民邮电出版社 / 2018-9 / 89.00元

Unity 是一款市场占有率非常高的商业游戏引擎,横跨25 个主流游戏平台。本书基于Unity 2018,结合2D 游戏开发和3D 游戏开发的案例,详细介绍了它的方方面面,内容涉及编辑器、游戏脚本、UGUI 游戏界面、动画系统、持久化数据、静态对象、多媒体、资源加载与优化、自动化与打包等。 本书适合初学者或者有一定基础的开发者阅读。一起来看看 《Unity 3D游戏开发(第2版)》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具