Go panic & recover:深入Go语言的异常拯救之道

想象一下这样的场景:你精心构建的Go服务正在生产环境中平稳运行,突然,一个看似无害的协程因一个意外的空指针解引用而崩溃------不是优雅地返回错误,而是像多米诺骨牌一样,引发连锁反应,导致整个服务进程瞬间崩溃。监控面板上的曲线断崖式下跌,告警信息瞬间刷屏。这就是panic在分布式系统中的真实威力,它不像错误那样温和,而是系统级的"紧急状态"。

在Go的世界里,panicrecover是一对特殊而强大的工具,它们处理的是那些"本不该发生"的异常情况。与许多其他语言通过异常处理机制不同,Go的设计哲学明确区分了可预期的错误 (使用多返回值处理)和不可恢复的异常(panic)。然而,正是这种设计选择,使得理解panic和recover的正确使用变得至关重要------用得好,它们是系统的安全网;用不好,则会掩盖问题,导致更隐蔽的bug。本篇文章将带你深入探索:

  1. panic的本质与机制:深入Go运行时,理解panic如何在栈帧间传播,以及它为何如此具有破坏性

  2. recover的正确使用姿势:不只是语法,更是设计哲学------何时该恢复,何时该让程序崩溃

  3. go panic和recover的底层实现原理 :揭开runtime包的神秘面纱,探究_panic结构体的内部表示、defer链与panic链的交互机制

通过这篇文章,你将不仅学会panic和recover的语法,更将掌握在复杂工程实践中正确使用它们的心智模型和设计原则。这不仅仅关乎代码能否运行,更关乎系统在极端情况下的韧性------毕竟,真正考验系统可靠性的,不是它能处理多少正常请求,而是当意外发生时,它能否优雅地失败并快速恢复。让我们开始这次深入Go异常处理机制的探索之旅,学习如何在代码的悬崖边建立可靠的防护栏,而不是在系统崩溃后收拾残局。

1 使用规则

1.1 panic - 程序异常终止

panic 是 Go 内置函数,用于触发程序异常终止流程。当遇到无法继续执行的严重错误时,可以调用 panic 中止当前 goroutine 的执行。panic可以由程序员显式调用 panic() 函数触发,也可以由程序在执行时触发异常而自动抛出:运行时错误(如数组越界、空指针解引用、类型断言失败);内置函数错误(如channel被重复关闭)。

Go 复制代码
// 基本用法
panic("something went wrong")

// 带自定义错误信息
panic(fmt.Errorf("database connection failed: %w", err))

// 使用任意类型
panic(42) // 不推荐但合法

1.2 recover - 异常恢复

recover 是内置函数,用于捕获并处理 panic,防止程序崩溃。需要注意的是recover必须与 defer 配合使用, recover 只在 defer 函数中有效,其函数的返回值取决于panic:无 panic 时返回 nil,有 panic 时返回 panic 的值。同时recover只能捕获同一 goroutine 内的 panic,更具体的是只能捕获当前函数及其调用的子函数中的 panic。(需要特别注意recover函数恢复后执行的代码点!!!

  • 如果程序出现以下的异常,是无法被捕获的:
    • 并发读写map
    • 栈的大小超过上限(1GB)
    • 内存溢出(堆内存不足)
  • 以下的异常只要程序编写得当则是能被捕获的:
    • 用户主动调用panic抛出的异常
    • 访问数组时,下标越界
    • 范围空指针
    • 断言类型失败
Go 复制代码
func safeFunction() {
    defer func() {
        if r := recover(); r != nil {
            // 处理 panic
            log.Printf("Recovered from panic: %v", r)
            // 可选:进行资源清理或错误上报
        }
    }()
    
    // 可能触发 panic 的代码
    riskyOperation()
}

​​​

1.3 注意事项

  • panic 只会触发当前 Goroutine 的 defer :执行下述代码时会发现 main 函数中的 defer 语句并没有执行,执行的只有当前 Goroutine 中的 defer。因为 defer 关键字对应的 runtime.deferproc 会将延迟调用函数与调用方所在 Goroutine 进行关联。所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数也是非常合理的。

    Go 复制代码
    func main() {
        defer println("in main")
        go func() {
            defer println("in goroutine")
            panic("")
        }()
    
        time.Sleep(1 * time.Second)
    }
    
    $ go run main.go
    in goroutine
    panic:...
  • recover 只有在 defer 中调用才会生效recover 只有在发生 panic 之后调用才会生效。如果recover 是在 panic 之前调用的,并不满足生效的条件,所以需要在 defer 中使用 recover 关键字。同时,在调用recover时,不能直接将其写在defer之后,需要使用匿名函数。`defer recover()`:这行代码的意思是,在`main`函数退出时,执行`recover()`的返回值(注意:这里并不是在延迟一个函数,而是立即计算`recover()`并延迟其返回值)。但是,在`main`函数正常执行期间,并没有发生panic,所以此时`recover()`返回`nil`,然后这个`nil`被延迟执行(相当于延迟执行一个空操作)。当`panic("Not implemented!")`发生时,由于延迟执行的已经是`recover()`的结果(即`nil`),而不是`recover`函数本身,因此它无法捕获后续的panic。

    Go 复制代码
    func main() {
        defer fmt.Println("in main")
        if err := recover(); err != nil {
            fmt.Println(err)
         }
        panic("unknown err")
    }
    
    $ go run main.go
    in main
    panic: unknown err
    
    goroutine 1 [running]:
    main.main()...
    exit status 2
    
    // 这里还需要特别注意的是,下述直接使用defer延迟注册recover函数也是无法正确捕捉异常的
    // 一定要将其放在匿名空函数中使用
    func main() {
    	defer recover() // 无法生效
        defer func() {
           if r := recover(); r != nil {
                // do something
           }
        }()
    	panic("Not implemented!")
    }
  • panic被recover正常捕获之后,在发生panic函数内部的后续代码不会被继续执行。而是会跳转至该函数调用者的函数栈帧,并执行本函数的deferreturn,执行本函数的defer链表以及调用者函数内部的后续代码。

    javascript 复制代码
    func test() {
    	defer func() {
    		if err := recover(); err != nil {
    			fmt.Println(err)
    		}
    	}()
    	defer fmt.Println("defer func 1")
    
    	panic("testing")
    	fmt.Println("panic is recovered") // panic虽然被捕获,但同一个函数内部的后续代码不会再执行
    }
    
    func main() {
    	test() // test函数内部的panic被正常捕获
    	fmt.Println("main func") // 继续执行后续代码
    	return
    }
    
    // 输出结果
    defer func 1
    testing
    main func
  • panic 允许在 defer 中嵌套多次调用: Go 语言中的 panic 是可以多次嵌套调用的,多次调用 panic 也不会影响 defer 函数的正常执行,所以使用 defer 进行收尾工作一般来说都是安全的。需要注意的是panic信息会按照其发生的顺序进行嵌套打印!

    Go 复制代码
    func main() {
        defer fmt.Println("in main") // defer_0
        defer func() { // defer_1
            defer func() {  defer_2
                panic("panic again and again")
            }()
            panic("panic again")
        }()
        panic("panic once")
    }
    
    $ go run main.go
    in main
    panic: panic once
            panic: panic again
            panic: panic again and again
    
    goroutine 1 [running]:...
    
    
    /*
    第一次 gopanic
       取出 defer_1,defer_1绑定panic once
       注册defer_2,此时defer链表中存在 defer_2 -> defer_1 -> defer_0
       
       第二次 gopanic
          新增一个panic,此时panic链表中存在 panic again -> panic once
          取出defer_2,defer_2绑定panic again,执行defer_2
          
          第三次 gopanic
             新增一个panic,此时panic链表中存在 panic again and again -> panic again -> panic once
             取出defer_2,defer_2已经绑定panic again,start = true,此时会将panic again标记为aborted,释放defer_2
             同理,取出defer_1,defer_1已经绑定panic once,start = true,此时会将panic once标记为aborted,释放defer_1
             取出defer_0进行执行,打印in main
    
             执行panic链表
    */
  • 如果panic发生多次嵌套,那么recover捕获到的错误将只会显示最后一个panic抛出的消息,前序的panic消息会被忽略,可以使用debug.Stack进行追踪执行顺序。

    Go 复制代码
    func test() {
    	defer func() {
    		if err := recover(); err != nil {
    			fmt.Println(err) // 只会打印panic,第一个panic的testing消息会被忽略
    			fmt.Println(string(debug.Stack()))
    		}
    	}()
    	defer func() {
    		panic("panic")
    	}()
    
    	panic("testing")
    	fmt.Println("panic is recovered")
    }
    
    func main() {
    	t1()
    	fmt.Println("main func")
    	return
    }
    
    panic
    goroutine 1 [running]:
    runtime/debug.Stack()
    	D:/Go/src/runtime/debug/stack.go:26 +0x5e
    main.t1.func1()
    	D:/Go_learning/dag/main.go:296 +0x54
    panic({0xb22180?, 0xb60628?})
    	D:/Go/src/runtime/panic.go:785 +0x132
    main.t1.func2()
    	D:/Go_learning/dag/main.go:300 +0x25
    panic({0xb22180?, 0xb60608?})
    	D:/Go/src/runtime/panic.go:785 +0x132
    main.t1()
    	D:/Go_learning/dag/main.go:303 +0x4e
    main.main()
    	D:/Go_learning/dag/main.go:308 +0x13
    
    main func
  • painc沿调用堆栈向外传递,直至被捕获,或进程崩溃。连续引发恐慌,仅最后一次可被捕获。

  • 先捕获panic,恢复执行,然后才返回panic的数据。recover之后,可再度引发panic,并再次被捕获。

  • 无论恢复与否,延迟调用函数总会被执行。延时调用中引发的panic,不影响后续延迟调用函数的执行。

  • panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer

  • recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;

2 底层实现

和defer类似,每个goroutine会保存属于自己 的panic信息,panic信息是抽象成_panic结构体,该结构体成员主要包含panic参数、被恢复后的下一个指令地址等。需要特别注意的是,虽然_panic也使用链表对其进行管理,但是在最终打印panic信息时,是按照panic发生的顺序依次打印的,即先进先出。_panic结构体成员变量的具体含义如下:

  • argpdefer调用时传入的参数的指针。
  • arg是调用panic时传入的参数。
  • link指向的是更早调用runtime._panic结构,也就是说painc可以被连续调用,它们之间形成链表
  • recovered 表示当前runtime._panic是否被recover恢复
  • aborted表示当前的panic是否被强行终止
Go 复制代码
type g struct {
    // ...
    _panic *_panic // 当前 panic 链表头
    _defer *_defer // defer 链表
}


type _panic struct {
    argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; panic当前要执行的defer参数空间地址
    arg       interface{}    // argument to panic panic 的参数
    link      *_panic        // link to earlier panic  链接到更早的 panic
    pc        uintptr        // where to return to in runtime if this panic is bypassed
    sp        unsafe.Pointer // where to return to in runtime if this panic is bypassed
    recovered bool           // whether this panic is over 表示panic是否被恢复
    aborted   bool           // the panic was aborted 表示panic是否被终止
    goexit    bool
}

当函数内部发生panic时,程序底层会调用runtime.gopanic函数开始处理异常:

  1. 首先检查panic 发生的环境是否合法
  2. 然后会创建一个_panic结构体插入到goroutine._panic链接的头部(因为panic也可以嵌套发生)
  3. 随后便会遍历goroutine._defer链表,依次调用defer注册的函数,执行完后会立即释放defer结构体占用的内存。如果在某次defer中调用了recover函数,则会将defer绑定的panic的recovered字段标记为true(绑定的panic是gopanic新创建的链表首个panic)
  4. 如果defer链表中有调用的recovery函数,则会恢复调用者的函数栈帧,即恢复执行调用者调用发生panic函数后续的代码(发生panic函数内部的后续代码不会再被执行),并执行runtime.deferreturn将此时goroutine上绑定的defer全部执行完毕(deferreturn函数详解可以参考Go defer(一):延迟调用的使用及其底层实现原理详解
  5. 如果遍历完整个defer链表,还没有调用recovery函数,则会调用fatalpanic函数来打印panic函数,并退出程序

2.1 gopanic函数

编译器会将关键字 panic 转换成 runtime.gopanic,该函数的执行过程包含以下几个步骤:

  1. 获取当前 goroutine:通过 `getg()` 获取当前 goroutine 的指针 `gp`。
  2. 检查 panic 发生的环境是否合法:
    1. 检查是否在系统栈上触发 panic(`gp.m.curg != gp`),如果是则抛出异常。
    2. 检查是否在内存分配过程中(`gp.m.mallocing != 0`),如果是则抛出异常。
    3. 检查是否在禁止抢占期间(`gp.m.preemptoff != ""`),如果是则抛出异常并打印原因。
    4. 检查是否持有锁(`gp.m.locks != 0`),如果是则抛出异常。
  3. 创建 panic 对象并添加到链表头部:
    1. 创建一个 `_panic` 结构体实例 `p`。
    2. 设置 `p.arg = e`(panic 的参数)。
    3. 将新的 panic 插入到当前 goroutine 的 panic 链表头部(`gp._panic` 指向新创建的 panic)。
  4. 增加正在处理的 panic defer 计数器:通过 `atomic.Xadd(&runningPanicDefers, 1)` 原子增加计数器。
  5. 处理 open-coded defer 帧(如果有): 调用 `addOneOpenDeferFrame` 记录 open-coded defer 的相关信息。
  6. 循环处理 defer 函数:
    1. 从当前 goroutine 的 defer 链表头部开始遍历 defer 函数(`gp._defer`)。
    2. 如果 defer 链表为空(`d == nil`),跳出循环,终止程序。
    3. 如果当前 defer 已经执行过(`d.started` 为 true),说明发生了 panic 嵌套,需要清理(早期的panic将不会被执行):
      1. 如果该 defer 关联了 panic,标记该 panic 为已终止(`aborted = true`)。
      2. 将该 defer 的 panic 关联置空(`d._panic = nil`)。
      3. 如果不是 open-coded defer,则释放该 defer 并继续处理下一个 defer。
    4. 标记当前 defer 为已开始(`d.started = true`),用于标记panic是否出现了嵌套。
    5. 将当前 panic 关联到 defer(`d._panic = &p`)。
  7. 执行 defer 函数:
    1. 如果是 open-coded defer:调用 `runOpenDeferFrame` 执行 defer 函数,并返回是否完成。
    2. 否则(普通 defer):
      1. 设置当前 panic 的参数指针(`p.argp = unsafe.Pointer(getargp(0))`)。
      2. 通过 `reflectcall` 调用 defer 函数。
      3. 执行完成后,清理 defer:
        1. 将 defer 的 panic 关联置空(`d._panic = nil`)。
        2. 如果执行完成(`done` 为 true),则释放 defer 资源(`freedefer(d)`)并从链表中移除。
  8. 检查 panic 是否被恢复: 如果 `p.recovered` 为 true(说明在 defer 中调用了 `recover` 并成功恢复):
    1. 调用 `mcall(recovery)` 切换到恢复执行流程,该函数会调度到m->g0进行执行,且不会返回
    2. 设置恢复的上下文信息(`gp.sigcode0` 和 `gp.sigcode1`),这里恢复执行的位置是调用者的函数栈帧信息(调用方函数返回之前并执行deferreturn)。
    3. 清理剩余的 defer 条目(特别是未开始的 open-coded defer)。
    4. 处理一些特殊情况(如 Goexit 的恢复)。
    5. 从 panic 链表中移除当前 panic(`gp._panic = p.link`,之前的panic也会被直接移除),同时将panic链表上所有标记aborted的panic结构体删除。
  9. 所有 defer 处理完毕,但 panic 未被恢复:
    1. 调用 `preprintpanics` 准备 panic 的字符串信息(调用 `Error` 或 `String` 方法)。
    2. 调用 `fatalpanic` 终止程序,打印 panic 信息和堆栈跟踪。
  10. 最后执行一个非法操作(`*(*int)(nil) = 0`)确保程序崩溃(实际上 `fatalpanic` 不会返回)。
Go 复制代码
// 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之后运行defered函数又发生了panic。
   //最新的panic会被挂入goroutine对应的g结构体对象的_panic链表的表头
   var p _panic   // 创建_panic结构体对象
   p.arg = e   // panic的参数
   p.link = gp._panic  // 保存下一个panic的地址
   gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 将此panic插入链表头部

   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 {
      d := gp._defer  // 取出当前groutine的_defer链表头部的defered函数
      if d == nil {
         break  //没有defer函数将会跳出循环,然后打印栈信息然后结束程序(panic没有被恢复)
      }

      // 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 {
      //到这里一定发生了panic嵌套,即在defered函数中又发生了panic
      //d.started = true是panic嵌套的充分条件,但并不是必要条件,也就是说即使d.started为false也是可能发生嵌套的
      //最近发生的一次panic并没有被recover,所以取消上一次发生的panic
         if d._panic != nil {
            d._panic.aborted = true
         }
         d._panic = nil
         if !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 = 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  // 用于判断是否发生了嵌套panic

      // 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.
      // 设置_defer结构体的_panic参数
      d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

      done := true
      if d.openDefer {
         done = runOpenDeferFrame(gp, d)
         if done && !d._panic.recovered {
            addOneOpenDeferFrame(gp, 0, nil)
         }
      } else {
         //在panic中记录当前panic的栈顶位置,用于recover判断
         p.argp = unsafe.Pointer(getargp(0))
         //通过reflectcall函数调用defered函数
         //如果defered函数再次发生panic而且并未被该defered函数recover,则reflectcall永远不会返回
         //如果defered函数并没有发生过panic或者发生了panic但该defered函数成功recover了新发生的panic,
         // 则此函数会返回继续执行后面的代码。
         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.pc
      sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
      // 将执行完的defer函数释放
      if done {
         d.fn = nil
         gp._defer = d.link
         freedefer(d)
      }
      
      //defered函数调用recover成功捕获了panic会设置p.recovered = true
      if p.recovered {
         gp._panic = p.link
         if 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)

         // 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._defer
         var prev *_defer
         if !done {
            // Skip our current frame, if not done. It is
            // needed to complete any remaining defers in
            // deferreturn()
            prev = d
            d = d.link
         }
         for d != nil {
            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 entries
               break
            }
            if d.openDefer {
               if prev == nil {
                  gp._defer = d.link
               } else {
                  prev.link = d.link
               }
               newd := d.link
               freedefer(d)
               d = newd
            } else {
               prev = d
               d = 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 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)

   // 终止整个程序
   fatalpanic(gp._panic) // should not return
   *(*int)(nil) = 0      // not reached
}

2.2 gorecover函数

该函数的实现很简单,如果当前 Goroutine 没有调用 panic,那么该函数会直接返回 nil,这也是崩溃恢复在非 defer 中调用会失效的原因。在正常情况下,它会修改 runtime._panicrecovered 字段(recover函数仅仅改变了当前groutine第一个panic对象的recovered的值),runtime.gorecover 函数中并不包含恢复程序的逻辑,程序的恢复是由 runtime.gopanic 函数负责的。

Go 复制代码
// 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:nosplit
func 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._panic
   if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
      p.recovered = true
      return p.arg
   }
   return nil
}

// Unwind the stack after a deferred function calls recover
// after a panic. Then arrange to continue running as though
// the caller of the deferred function returned normally.
func recovery(gp *g) {
   // Info about defer passed in G struct.
   sp := gp.sigcode0
   pc := gp.sigcode1

   // d's arguments need to be in the stack.
   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")
   }

   // Make the deferproc for this d return again,
   // this time returning 1. The calling function will
   // jump to the standard return epilogue.
   gp.sched.sp = sp
   gp.sched.pc = pc
   gp.sched.lr = 0
   gp.sched.ret = 1
   gogo(&gp.sched)
}

2.3 嵌套流程浅析

Go 复制代码
func main() {

	defer fmt.Println("in main") // defer_0
	defer func() { // defer_1
		defer func() { // defer_2
			panic("panic again and again") // panic_3
		}()
		fmt.Println("step 2")
		panic("panic again") // panic_2
	}()
	
	defer func() { // defer_3
        recover()
    }()
	panic("panic once") // panic_0
	panic("panic 1") // panic_1,不会被执行
}

// 上述程序执行的结果如下
step 2
in main
panic: panic again
	panic: panic again and again

/*
main
    gopanic // 第一次panic("panic once")触发
        1. 取出 defer_3,设置 started
        2. 执行 defer_3 
            - 执行 recover,panic_0 字段被设置 recovered
        3. 把 defer_3 从链表中摘掉
        4. 把panic_0 从链表中摘掉
        4. 执行 recovery ,重置 pc 寄存器,跳转到 defer_0 注册时候,携带的指令,一般是跳转到 deferreturn 上面几个指令
 
    // 跳出 gopanic 的递归嵌套,执行到 deferreturn 的地方;
    defereturn
 
        1. 遍历 defer 函数链,取出 defer_1,设置 started    
        2. 执行 defer_1,注册defer_2,打印 step 2
            gopanic // 第二次
                1. 取出defer_1,因为started已经被标记为true,需要执行后摘掉;
                2. panic_2.aborted = true
                3. 遍历defer取出defer_2,设置 started 
                     gopanic // 第三次
                        1. defer_2 已经被标记started,所以执行后,摘掉
                        2. panic_2.aborted = true
                        3. 摘掉 defer_0,打印in main,链表为空,跳出 for 循环
                        4. 执行 fatalpanic
                            - exit(2) 退出进程
   
*/
相关推荐
ttwuai1 小时前
GoFrame + Vue3 后台管理系统实战:CRUD、权限和菜单如何少写重复代码
golang
IT_陈寒1 小时前
Vue的v-if和v-show,我竟然用错了这么久
前端·人工智能·后端
鹏易灵1 小时前
C++——7.类与对象,掌握封装、继承、多态.详解
开发语言·c++·算法
一路向北North1 小时前
Spring Security OAuth2.0(16):密码模式
java·后端·spring
水无痕simon1 小时前
5 多表操作
java·开发语言·数据库
初阳7851 小时前
【Qt】系统相关(2)——文件
开发语言·qt
IT_陈寒1 小时前
React状态更新延迟搞晕我,原来是闭包在捣鬼
前端·人工智能·后端
Ricky_Theseus2 小时前
Trie 字典树:前缀匹配利器
开发语言·c#
cui_ruicheng2 小时前
Python从入门到实战(三):流程控制与循环语句
开发语言·python