recover.panic.defer.2021.03.03

Defer, Panic, and Recover

在 Go 语言中,recover 和 panic 的关系是什么?

我们先看一个基础的例子,在 main 方法体中启动一个协程,在协程内部主动调用 panic。程序的执行会被中断了,但有个疑问,为什么在别的协程里调用了 panic,要让 main 协程也退出呢?

func main() {go func() {panic("call panic")}()for{}}

针对这种情况,我们引入 recover 方法。这里故意写了一段错误的代码,代码如下,运行的结果会怎么样呢?能 recover 住 panic 吗?

程序执行还是被中断了,recover 并没有起作用。因为 recover 没有写在 defer 函数里。实际上,recover 和 defer 联用,并且不跨协程,才能真正的拦截 panic 事件。

func main() {go func() {        // 追加的代码if r := recover(); r != nil {fmt.Println(r)}panic("call panic")}()for{}}

正确的写法如下。这里描述的内容在 Go 博客Defer, Panic, and Recover 有详细解释。

func main() {go func() {defer func() {if r := recover(); r != nil {fmt.Println(r)}}()panic("call panic")}()fmt.Println("come on")}

Panic 和 Recover 的联系

在 panic 的过程中, panic 传入的参数用来作为 recover 函数的返回。

下面的例子中,声明了一个 inner 类型的结构体。panic 的时候,我们指定的入参是一个 inner 结构体变量,inner 的 Msg 成员值为 Thank。然后,我们对 recover 的返回做断言处理(因为返回类型为 interface),直接断言它为 inner 值类型。

工作中,我们经常遇到的切片下标越界,go 在处理到这种类型的 panic 时,默认传递的就是 runtime 包下的 boundsError(A boundsError represents an indexing or slicing operation gone wrong.)。

type inner struct {Msg string}func main() {defer func() {if r := recover(); r != nil {fmt.Print(r.(inner))}}()panic(inner{Msg: "Thank"})}

panic 嵌套

当程序 panic 之后,调用 defer 函数时又触发了程序再次 panic。在程序的错误栈输出信息中,三处 panic 的错误信息都输出了。

我们不使用任何 recover ,查看 panic 的输出信息。从代码末尾的注释中可以发现,三个 panic 都触发了,而且输出中也包含了三个 panic 的信息。

func main() {    go func() {        // defer 1        defer func() {            // defer 2            defer func() {                panic("call panic 3")            }()            panic("call panic 2")        }()        panic("call panic 1")    }()    for{}}//output://panic: call panic 1//        panic: call panic 2//        panic: call panic 3////goroutine 18 [running]://main.main.func1.1.1()//        /Users/fuhui/Desktop/panic/main.go:10  0x39

接下来,我们代码做 recover 处理,观察程序的输出情况。上面的示例中,程序依次触发了 panic 1、2、3。现在我们修改代码,对 panic 3 做捕获处理,程序还会继续 panic 吗?

我们在代码中又嵌套追加了第三个 defer,对 panic 3 进行捕获。从代码的输出结果中,我们可以发现,代码还是 panic 了。

虽然我们还不了解具体的实现,但至少我们可以明白:Go 程序中的 panic 都需要被 recover 处理掉,才不会触发程序终止。如果只处理链路中的最后一个,程序还是会异常终止。

我们稍作调整,在 defer 3 中再写三个 recover 语句可行吗?这样也是不可行的,defer、panic、recover 需要是一体的,大家可以自行验证。

func main() {    go func() {        // defer 1        defer func() {            // defer 2            defer func() {                // defer 3                defer func() {                    if r := recover(); r != nil{                        fmt.Println("recover", r)                    }                }()                panic("call panic 3")            }()            panic("call panic 2")        }()        panic("call panic 1")    }()    for{}}//output://recover panic 3//panic: call panic 1//        panic: call panic 2////goroutine 18 [running]:

源码

Go 源码版本

确定 Go 源码的版本

➜  server go versiongo version go1.15.1 darwin/amd64

gopanic

我们来看 panic 的类型结构:

arg 作为 panic 是的入参,对应我们调用 panic 函数是的入参。在后续 recover 的时候会返回这个参数。

link 作为一个 _panic 类型指针,通过这个类型,可以说明:在 Goroutine 内部 _panic 是按照链表的结构存储的。在一个 goroutine 内,可能会出现多个 panic,但这些 panic 信息都会被存储。

// A _panic holds information about an active panic.//// This is marked go:notinheap because _panic values must only ever// live on the stack.//// The argp and link fields are stack pointers, but don't need special// handling during stack growth: because they are pointer-typed and// _panic values only live on the stack, regular stack pointer// adjustment takes care of them.////go:notinheaptype _panic struct {argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblinkarg       interface{}    // argument to paniclink      *_panic        // link to earlier panicpc        uintptr        // where to return to in runtime if this panic is bypassedsp        unsafe.Pointer // where to return to in runtime if this panic is bypassedrecovered bool           // whether this panic is overaborted   bool           // the panic was abortedgoexit    bool}

gopanic 方法体代码比较长,我们直接在注释中对它进行标注和分析

// The implementation of the predeclared function panic.func gopanic(e interface{}) {gp := getg()if gp.m.curg != gp {print("panic: ")printany(e)print("\n")throw("panic on system stack")}if gp.m.mallocing != 0 {print("panic: ")printany(e)print("\n")throw("panic during malloc")}if gp.m.preemptoff != "" {print("panic: ")printany(e)print("\n")print("preempt off reason: ")print(gp.m.preemptoff)print("\n")throw("panic during preemptoff")}if gp.m.locks != 0 {print("panic: ")printany(e)print("\n")throw("panic holding locks")}        // 创建了这个 panic 对象,将这个 panic 对象的 link 指针指向当前 goroutine 的 _panic 列表    // 说白了就是一个链表操作,将当前 panic 插入到当前 goroutine panic 链表的首位置var p _panicp.arg = ep.link = gp._panicgp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))atomic.Xadd(&runningPanicDefers, 1)// By calculating getcallerpc/getcallersp here, we avoid scanning the// gopanic frame (stack scanning is slow...)addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))for {        // 循环获取 gp 的 defer,这里不展开,但 _defer 也是跟 _panic 一样按照链表结构进行存储的。d := gp._deferif d == nil {break}// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),// take defer off list. An earlier panic will not continue running, but we will make sure below that an// earlier Goexit does continue running.if d.started {if d._panic != nil {d._panic.aborted = true}d._panic = nilif !d.openDefer {// For open-coded defers, we need to process the// defer again, in case there are any other defers// to call in the frame (not including the defer// call that caused the panic).d.fn = nilgp._defer = d.linkfreedefer(d)continue}}// Mark defer as started, but keep on list, so that traceback// can find and update the defer's argument frame if stack growth// or a garbage collection happens before reflectcall starts executing d.fn.d.started = true// Record the panic that is running the defer.// If there is a new panic during the deferred call, that panic// will find d in the list and will mark d._panic (this panic) aborted.d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))done := trueif d.openDefer {done = runOpenDeferFrame(gp, d)if done && !d._panic.recovered {addOneOpenDeferFrame(gp, 0, nil)}} else {p.argp = unsafe.Pointer(getargp(0))reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))}p.argp = nil// reflectcall did not panic. Remove d.if gp._defer != d {throw("bad defer entry in panic")}d._panic = nil// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic//GC()pc := d.pcsp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copyif done {d.fn = nilgp._defer = d.linkfreedefer(d)}if p.recovered {gp._panic = p.linkif gp._panic != nil && gp._panic.goexit && gp._panic.aborted {// A normal recover would bypass/abort the Goexit.  Instead,// we return to the processing loop of the Goexit.gp.sigcode0 = uintptr(gp._panic.sp)gp.sigcode1 = uintptr(gp._panic.pc)mcall(recovery)throw("bypassed recovery failed") // mcall should not return}atomic.Xadd(&runningPanicDefers, -1)if done {// Remove any remaining non-started, open-coded// defer entries after a recover, since the// corresponding defers will be executed normally// (inline). Any such entry will become stale once// we run the corresponding defers inline and exit// the associated stack frame.d := gp._defervar prev *_deferfor d != nil {if d.openDefer {if d.started {// This defer is started but we// are in the middle of a// defer-panic-recover inside of// it, so don't remove it or any// further defer entriesbreak}if prev == nil {gp._defer = d.link} else {prev.link = d.link}newd := d.linkfreedefer(d)d = newd} else {prev = dd = d.link}}}gp._panic = p.link// Aborted panics are marked but remain on the g.panic list.// Remove them from the list.for gp._panic != nil && gp._panic.aborted {gp._panic = gp._panic.link}if gp._panic == nil { // must be done with signalgp.sig = 0}// Pass information about recovering frame to recovery.gp.sigcode0 = uintptr(sp)gp.sigcode1 = pcmcall(recovery)throw("recovery failed") // mcall should not return}}// ran out of deferred calls - old-school panic now// Because it is unsafe to call arbitrary user code after freezing// the world, we call preprintpanics to invoke all necessary Error// and String methods to prepare the panic strings before startpanic.preprintpanics(gp._panic)fatalpanic(gp._panic) // should not return*(*int)(nil) = 0      // not reached}

gorecover

源码中的 getg() 方法返回当前的 goroutine,之后是获取当前 Go 的 panic 信息。紧接着 if 判断,如果条件符合的话,将这个 panic 对象的 recovered 属性设置为 true,也就是标记为被处理了,并返回的是这个 panic 的参数。如果 if 条件不满足的话,表示没有 panic 对象被捕获,返回空。

// The implementation of the predeclared function recover.// Cannot split the stack because it needs to reliably// find the stack segment of its caller.//// TODO(rsc): Once we commit to CopyStackAlways,// this doesn't need to be nosplit.//go:nosplitfunc gorecover(argp uintptr) interface{} {// Must be in a function running as part of a deferred call during the panic.// Must be called from the topmost function of the call// (the function used in the defer statement).// p.argp is the argument pointer of that topmost deferred function call.// Compare against argp reported by caller.// If they match, the caller is the one who can recover.gp := getg()p := gp._panicif p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {p.recovered = truereturn p.arg}return nil}

注:recover函数捕获的是祖父一级调用函数栈的异常。必须要和有异常的栈帧只隔一个栈帧,recover函数才能正捕获异常。

来源:https://www.icode9.com/content-4-883851.html

(0)

相关推荐

  • 详解并发编程基础之原子操作(atomic包)

    Go语言中文网 今天 以下文章来源于Golang梦工厂 ,作者AsongGo Golang梦工厂Asong是一名Golang开发工程师,专注于Golang相关技术:Golang面试.Beego.Gin ...

  • Go中看似简单的WaitGroup源码设计,竟然暗含这么多知识?

    原创 机器铃砍菜刀 Golang技术分享 1周前 Go语言提供的协程goroutine可以让我们很容易地写出多线程程序,但是,如何让这些并发执行的goroutine得到有效地控制,这是我们需要探讨的问 ...

  • 两万字长文带你深入Go语言GC源码(下)

    作者:luozhiyun,腾讯IEG后台开发工程师 博客: https://www.luozhiyun.com/archives/475 本文使用的 Go 的源码1.15.7 创建后台标记 Worke ...

  • 在Go中,你犯过这些错误吗

    Go语言中文网 今天 以下文章来源于吴亲强的深夜食堂 ,作者吴亲库里 吴亲强的深夜食堂关注一些奇奇怪怪的设计,分享一些有有趣趣的生活 迭代器变量上使用 goroutine 这算高频吧. package ...

  • Go 数据结构和算法篇(二):栈

    Go语言中文网 今天 以下文章来源于xueyuanjun ,作者xueyuanjun 从逻辑角度来说,数组和链表都是线性结构(就是排成一条线的结构,只有前后两个方向,非线性结构包括树.图等,后面会讲到 ...

  • Go 最细节篇 — chan 为啥没有判断 close 的接口 ?

    大纲 Go 为什么没有判断 close 的接口? Go 关闭 channel 究竟做了什么? `closechan` 一个判断 chan 是否 close 的函数 思考方法一:通过"写&qu ...

  • 多图详解Go中的Channel源码

    chan介绍 package mainimport "fmt" func main() { c := make(chan int) go func() { c <- 1 // ...

  • Go操作Redis实战

    目录 安装Redis客户端 连接redis 基本指令 Keys():根据正则获取keys Type():获取key对应值得类型 Del():删除缓存项 Exists():检测缓存项是否存在 Expir ...

  • Go 专栏|错误处理:defer,panic 和 recover

    最近校招又开始了,我也接到了一些面试工作,当我问「你觉得自己有什么优势」时,十个人里有八个的回答里会有一条「精力充沛,能加班」. 怪不得国家都给认证了:新生代农民工.合着我们这根本就不是什么脑力劳动者 ...

  • go编程:说说channel哪些事

    channel是什么 channel中文翻译为通道,它是Go语言内置的数据类型,使用channel不需要导入任何包,像int/float一样直接使用.它主要用于goroutine之间的消息传递和事件通 ...

  • Golang研学:defer!如何掌握并用好(延迟执行)

    defer:在函数A内用defer关键字调用的函数B会在在函数A return后执行. 先看一个基础的例子,了解一下defer的效果 func main() { fmt.Println('in mai ...

  • Go Fiber 框架系列教程 03:中间件

    2021-10-18 阅读本文大概需要 10 分钟. 大家好,我是 polarisxu. Middleware(中间件) 是一个 Web 框架重要的组成部分,通过这种模式,可以方便的扩展框架的功能.目 ...

  • 学习channel设计:从入门到放弃

    前言 哈喽,大家好,我是asong.终于回归了,停更了两周了,这两周一直在搞留言号的事,经过漫长的等待,终于搞定了.兄弟们,以后就可以在留言区尽情开喷了,只要你敢喷,我就敢精选