找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3273

积分

0

好友

423

主题
发表于 9 小时前 | 查看: 1| 回复: 0

panic的中文含义为“恐慌”或“惊慌”。当Go程序发生panic时,会立即结束当前Goroutine的执行,进而可能触发整个程序的崩溃。幸运的是,内置函数recover()可以捕获panic并使程序流程回到正轨。

panic()函数

panic是Go语言的一个内置函数,其函数签名如下:

源码位置: src/builtin/builtin.go

// The panic built-in function stops normal execution of the current
// goroutine. When a function F calls panic, normal execution of F stops
// immediately. Any functions whose execution was deferred by F are run in
// the usual way, and then F returns to its caller. To the caller G, the
// invocation of F then behaves like a call to panic, terminating G's
// execution and running any deferred functions. This continues until all
// functions in the executing goroutine have stopped, in reverse order. At
// that point, the program is terminated with a non-zero exit code. This
// termination sequence is called panicking and can be controlled by the
// built-in function recover.
//
// Starting in Go 1.21, calling panic with a nil interface value or an
// untyped nil causes a run-time error (a different panic).
// The GODEBUG setting panicnil=1 disables the run-time error.
func panic(v any)

它接受一个任意类型(any,即interface{})的参数。这个参数将在程序崩溃时通过另一个内置函数print(...)打印出来。如果在panic向上传播的过程中,任意一个defer函数执行了recover(),那么这个参数也会成为recover()的返回值。

panic既可以由程序员主动通过内置函数触发,Go运行时在遇到诸如内存越界、空指针解引用等严重错误时也会自动触发。

工作流程

Go panic触发时defer执行流程图

上面的流程图清晰地展示了panic的处理流程。程序启动了两个协程,如果某个协程执行过程中产生了panic,程序将立即转向执行当前函数中已注册的defer函数。当前函数的defer执行完毕后,会继续处理其调用者(上层函数)中的defer,以此类推。当该协程中所有defer处理完成后,如果panic未被恢复,程序便会退出。

需要注意的几点

  • panic会递归地执行当前协程调用链中所有的defer函数,顺序与正常函数退出时一致(后进先出,LIFO)。
  • panic只会处理当前协程中的defer,不会影响其他协程。
  • 当前协程中的defer全部处理完毕后,若panic未被recover,则会触发程序退出。

源码剖析:从编译到执行

我们通过一个简单的例子来看看panic在底层是如何实现的。

示例代码:

package Concurrent

func compile() {
 panic("aa")
}

通过go tool compile -S命令编译上述代码,我们可以查看其生成的汇编指令。

Go panic编译后的汇编代码

从汇编输出中可以看到,panic(“aa”)语句最终被编译成了 CALL runtime.gopanic(SB)。这揭示了panic()内置函数的真身实际上是runtime包中的gopanic()函数。

源码位置: src/runtime/panic.go

核心数据结构:_panic

在深入gopanic函数之前,我们需要了解其操作的核心数据结构_panic

源码位置: src/runtime/runtime2.go

type _panic struct {
 argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
 arg       any            // argument to panic
 link      *_panic        // link to earlier panic

 // startPC and startSP track where _panic.start was called.
 startPC uintptr
 startSP unsafe.Pointer

 // The current stack frame that we're running deferred calls for.
 sp unsafe.Pointer
 lr uintptr
 fp unsafe.Pointer

 // retpc stores the PC where the panic should jump back to, if the
 // function last returned by _panic.next() recovers the panic.
 retpc uintptr

 // Extra state for handling open-coded defers.
 deferBitsPtr *uint8
 slotsPtr    unsafe.Pointer

 recovered bool // whether this panic has been recovered
 goexit    bool
 deferreturn bool
}

各字段含义解析:

  • argp: 指向正在panic的defer函数参数的指针。
  • arg: panic的触发参数,即panic(x)中的x
  • link: 指向更早触发的panic的指针,用于支持嵌套panic(例如在defer函数中再次触发panic)。
  • startPC/startSP: 记录_panic.start方法被调用时的程序计数器(PC)和栈指针(SP)。
  • sp/lr/fp: 用于定位和回溯当前执行defer的栈帧上下文(栈指针、链接寄存器、帧指针)。
  • retpc: 这是recover能让程序“恢复执行”的核心——它记录了当panic被恢复后,执行流应该跳转回的地址。
  • deferBitsPtr / slotsPtr: 用于处理“开放编码defer”优化相关的状态和函数槽。
  • recovered: 标记该panic是否已被recover()恢复。
  • goexit: 标记此panic是否由runtime.Goexit()触发(Goexit本质上是一种特殊的panic)。
  • deferreturn: 标记是否是deferreturn触发的“伪panic”,仅用于执行defer链,不表示真正的崩溃。

panic与recover机制流程图

gopanic函数深度分析

runtime.gopanic()是整个panic机制的核心。它的主要任务是遍历并执行当前Goroutine的defer链表。只有在所有defer处理完毕且未被recover时,才会触发程序退出。由于defer函数内部有可能再次触发panic(产生新的gopanic调用),或者通过recover()恢复当前panic,所以其逻辑需要精心设计。

以下是gopanic函数的主体流程分析(代码已做精简和注释):

func gopanic(e any) {
 // 1. 处理 panic(nil) 的特殊情况
 if e == nil {
  if debug.panicnil.Load() != 1 {
   e = new(PanicNilError) // Go 1.21+ 行为:为nil panic创建一个默认错误
  } else {
   panicnil.IncNonDefault() // 仅统计,不创建错误(GODEBUG=panicnil=1时)
  }
 }

panic(nil)处理逻辑代码截图

 gp := getg() // 2. 获取当前 Goroutine

 // 3. 一系列安全检查,在非法状态下触发panic会直接崩溃
 // 安全检查1:禁止在系统栈上触发 panic
 if gp.m.curg != gp {
  print("panic: ")
  printpanicval(e)
  print("\n")
  throw("panic on system stack") // 直接崩溃,无法恢复
 }

系统栈panic检查代码

 // 安全检查2:禁止在内存分配期间 panic
 if gp.m.malloccing != 0 {
  print("panic: ")
  printpanicval(e)
  print("\n")
  throw("panic during malloc")
 }

内存分配期间panic检查代码

 // 安全检查3:禁止在“禁止抢占”期间 panic
 if gp.m.preemptoff != "" {
  print("panic: ")
  printpanicval(e)
  print("\n")
  print("preempt off reason: ")
  print(gp.m.preemptoff)
  print("\n")
  throw("panic during preemptoff")
 }

禁止抢占期间panic检查代码

 // 安全检查4:禁止在持有运行时锁期间 panic
 if gp.m.locks != 0 {
  print("panic: ")
  printpanicval(e)
  print("\n")
  throw("panic holding locks")
 }

 // 4. 创建 _panic 实例,并初始化核心字段
 var p _panic
 p.arg = e // 存储panic参数,即panic(e)中的e

创建_panic实例代码

 // 5. 增加正在panic中执行的defer计数器(用于监控/调试)
 runningPanicDefers.Add(1)

 // 6. 初始化panic上下文:关联当前栈帧,准备遍历执行当前函数的defer链表
 p.start(sys.GetCallerPC(), unsafe.Pointer(sys.GetCallerSP()))

 // 7. 核心循环:遍历并执行所有 defer 函数
 for {
  fn, ok := p.nextDefer() // 从defer链表中取出下一个待执行的defer函数(LIFO顺序)
  if !ok {
   break // 遍历完毕,退出循环
  }
  fn() // 执行defer函数。若其中调用recover()会标记p.recovered=true
 }

defer执行循环代码截图

 // 8. 跟踪(trace)相关处理
 // If we're tracing, flush the current generation to make the trace more readable.
 if traceEnabled() {
  traceAdvance(false)
 }

 // 9. 所有 defer 执行完毕且未被 recover → 触发致命 panic
 // ran out of deferred calls - old-school panic now
 preprintpanics(&p)
 fatalpanic(&p) // should not return

 // 10. 不可达代码,防止编译警告
 *(*int)(nil) = 0 // not reached
}

panic终结处理代码截图

完整流程图总结

为了更直观地理解整个panic的决策与执行流程,可以参考下面这幅综合流程图:

Go panic完整处理流程图

这幅图从业务代码调用panic(e)开始,历经运行时转换、安全检查、defer遍历、recover判断,直到最终程序恢复或崩溃,清晰地描绘了Go语言异常处理的核心路径。理解这些底层机制,能帮助开发者在云栈社区等平台更深入地讨论和编写出更健壮的Go程序。

春风若有怜花意,可否许我再少年。




上一篇:零成本前端跨域实战:用Cloudflare Workers代理Coze工作流API
下一篇:2024/2025年十大高颜值Linux发行版盘点:从桌面美化到稳定生产
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 20:50 , Processed in 0.305329 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表