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运行时在遇到诸如内存越界、空指针解引用等严重错误时也会自动触发。
工作流程

上面的流程图清晰地展示了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命令编译上述代码,我们可以查看其生成的汇编指令。

从汇编输出中可以看到,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链,不表示真正的崩溃。

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时)
}
}

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

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

// 安全检查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")
}

// 安全检查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

// 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
}

// 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(e)开始,历经运行时转换、安全检查、defer遍历、recover判断,直到最终程序恢复或崩溃,清晰地描绘了Go语言异常处理的核心路径。理解这些底层机制,能帮助开发者在云栈社区等平台更深入地讨论和编写出更健壮的Go程序。
春风若有怜花意,可否许我再少年。