panic & recover

深入理解Go的panic和recover

开场白

深入理解 Go panic and recover

谈谈 panic 和 recover 的原理

注:上面的链接的文章非常棒,需要大家看完链接文章后,才能往下看

第一个例子

假如你看明白了上述的文章,那我举两个例子:

package main

 

import (

    "fmt"

)

 

func main() {

    fmt.Println("ok1")

     defer func() {   // defer1

        bb := recover()

        fmt.Println("bb =", bb)

    }()

    defer func() {  // defer2

        fmt.Println("ok2")

        panic("panic again")

    }()

    panic("panic1")

}

 

ok1

ok2

bb = panic again

这个例子说明了啥?为什么两个panic,recover了一次,程序却能正常运行,那我们就从源码上来分析。

我们暂且命名panic1和panic2,以及defer1和defer2

 

通过上述文章的了解,大家已经知道当代码显示调用panic的时候,真正实现这个功能的函数是

func gopanic(e interface{}) {

       ......

       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

              }

 

              // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),

              // take defer off list. The earlier panic or Goexit will not continue running.

              if d.started {

                     if d._panic != nil {

                            d._panic.aborted = true

                     }

                     d._panic = nil

                     d.fn = nil

                     gp._defer = d.link

                     freedefer(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)))

 

              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

              d.fn = nil

              gp._defer = d.link

 

              // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic

              //GC()

 

              pc := d.pc

              sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy

              freedefer(d)

              if p.recovered {

                     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 signal

                            gp.sig = 0

                     }

                     // Pass information about recovering frame to recovery.

                     gp.sigcode0 = uintptr(sp)

                     gp.sigcode1 = pc

                     mcall(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)

       startpanic()

       printpanics(gp._panic)

       dopanic(0)       // should not return

       *(*int)(nil) = 0 // not reached

}

15行的d.started是一个很容易让人忽略的属性,

当panic1触发首先调用的是defer2时发生赋值:

defer2.started = true, 

defer2.panic = panic1,

此时调用​reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))​这里需要注意,将一去不复返了,为什么呢?往下分析

    defer func() {  // defer2

        fmt.Println("ok2")

        panic("panic again")

    }()

此时reflectall调用defer2里面的代码会触发再次触发gopanic,从gp._defer链表的表头defer2开始,因为defer2.started=true已经被panic1调用时赋值,所以此时进入如下逻辑

// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),

// take defer off list. The earlier panic or Goexit will not continue running.

if d.started {

   if d._panic != nil {

      d._panic.aborted = true

   }

   d._panic = nil

   d.fn = nil

   gp._defer = d.link

   freedefer(d)

   continue

}

把panic1.aborted设置为true,然后continue重新获取链表的defer1,并且此时调用defer1的时候发现了recover方法, 在这里解决的是panic2

if p.recovered {

   atomic.Xadd(&runningPanicDefers, -1)

 

   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 signal

      gp.sig = 0

   }

   // Pass information about recovering frame to recovery.

   gp.sigcode0 = uintptr(sp)

   gp.sigcode1 = pc

   mcall(recovery)

   throw("recovery failed") // mcall should not return

 

其中需要注意的是循环处理

  for gp._panic != nil && gp._panic.aborted {

      gp._panic = gp._panic.link

   }

 

因为panic1.aborted=true, 所以将其从panic链表上剔除掉,这个时候gp._panic链表此时为空了,所以也不会再异常了。

总结:如果两个或者多个panic所关联的协程的defer属于同一个,即可一个recover成功。

 

第二个例子

package main

 

import (

        "fmt"

)

func main() {

        fmt.Println("ok1")

        defer func() {  // defer1

                fmt.Println("ok2")

                defer func() {  //defer2

                        fmt.Println("ok5")

                        aa := recover()

                        fmt.Println("ok4", aa)

                }()

                panic("panic again")

        }()

        panic("panic1")

}

 

ok1

ok2

ok5

ok4 panic again

panic: panic1

 

goroutine 1 [running]:

main.main()

    /tmp/test.go:35 +0xbd

这个例子捕获了panic2,未捕获panic1,从而程序崩溃。

 

我们来分析下

当panic1触发首先调用的是gb._defer链表的表头defer1,此时赋值:

defer1.started = true, 

defer1.panic = panic1,

然后调用defer里面的延迟函数​reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))​,

在执行defer内延迟函数时新增了一个defer2,此刻defer2会挂再gb._defer后,同时又出发了gopanic,但是注意的是,此时可gopanic对应的进入的逻辑是从表头取出的是defer2(这里是与第一个例子不同的关键所在)

defer2.started=true,

defer2.panic=panic2,

然后在defer2中发现了recover并恢复过程中,panic2.recovery=true,得到了恢复,但是panic链表中的panic1并未得到解决,遍历gb._defer链表后,没有recovery函数,所以最终panic发生。

### 介绍 在Go语言中,`panic` 和 `recover` 是用于处理异常情况的重要机制。`panic` 是一个内置函数,用于中止程序并引发恐慌。当调用 `panic` 时,程序会停止当前函数的执行,并开始回溯调用栈,寻找最近的 `recover` 调用。`recover` 也是一个内置函数,用于捕获 `panic` 并恢复程序的执行。如果在发生 `panic` 时调用了 `recover`,程序将继续执行,并且 `panic` 的原因将被存储在 `recover` 的返回值中 [^1][^2]。 ### 使用方法 #### `panic` 的使用 `panic` 函数可以接收一个参数,该参数将作为 `panic` 的原因。当发生 `panic` 时,程序将停止执行,并开始寻找最近的 `recover` 调用。如果找不到 `recover` 调用,程序将打印 `panic` 的原因并退出。示例代码如下: ```go package main func main() { panic("发生了一个严重的错误") } ``` #### `recover` 的使用 `recover` 通常在 `defer` 函数中使用,因为 `defer` 函数会在函数返回之前执行,这样可以确保在 `panic` 发生时能够捕获到它。示例代码如下: ```go package main import "fmt" func main() { defer func() { err := recover() if err != nil { fmt.Println("捕获到 panic:", err) } }() panic("发生了一个错误") } ``` ### 区别 - **功能不同**:`panic` 用于主动触发程序的异常状态,使程序进入恐慌模式;而 `recover` 用于捕获 `panic`,并使程序从恐慌状态中恢复,继续执行后续代码 [^2]。 - **调用时机不同**:`panic` 可以在任何需要的地方调用,以表示程序遇到了无法处理的错误;而 `recover` 通常在 `defer` 函数中调用,以确保在 `panic` 发生时能够被捕获 [^2][^4][^5]。 - **返回值不同**:`panic` 函数没有返回值;而 `recover` 函数返回 `interface{}` 类型的值,如果没有发生 `panic`,则返回 `nil` [^2][^4][^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值