想象一下这样的场景:你精心构建的Go服务正在生产环境中平稳运行,突然,一个看似无害的协程因一个意外的空指针解引用而崩溃------不是优雅地返回错误,而是像多米诺骨牌一样,引发连锁反应,导致整个服务进程瞬间崩溃。监控面板上的曲线断崖式下跌,告警信息瞬间刷屏。这就是panic在分布式系统中的真实威力,它不像错误那样温和,而是系统级的"紧急状态"。
在Go的世界里,panic和recover是一对特殊而强大的工具,它们处理的是那些"本不该发生"的异常情况。与许多其他语言通过异常处理机制不同,Go的设计哲学明确区分了可预期的错误 (使用多返回值处理)和不可恢复的异常(panic)。然而,正是这种设计选择,使得理解panic和recover的正确使用变得至关重要------用得好,它们是系统的安全网;用不好,则会掩盖问题,导致更隐蔽的bug。本篇文章将带你深入探索:
-
panic的本质与机制:深入Go运行时,理解panic如何在栈帧间传播,以及它为何如此具有破坏性
-
recover的正确使用姿势:不只是语法,更是设计哲学------何时该恢复,何时该让程序崩溃
-
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 的延迟调用函数也是非常合理的。Gofunc 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。Gofunc 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链表以及调用者函数内部的后续代码。
javascriptfunc 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信息会按照其发生的顺序进行嵌套打印!Gofunc 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进行追踪执行顺序。
Gofunc 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结构体成员变量的具体含义如下:
argp是defer调用时传入的参数的指针。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函数开始处理异常:
- 首先检查panic 发生的环境是否合法
- 然后会创建一个_panic结构体插入到goroutine._panic链接的头部(因为panic也可以嵌套发生)
- 随后便会遍历goroutine._defer链表,依次调用defer注册的函数,执行完后会立即释放defer结构体占用的内存。如果在某次defer中调用了recover函数,则会将defer绑定的panic的recovered字段标记为true(绑定的panic是gopanic新创建的链表首个panic)
- 如果defer链表中有调用的recovery函数,则会恢复调用者的函数栈帧,即恢复执行调用者调用发生panic函数后续的代码(发生panic函数内部的后续代码不会再被执行),并执行runtime.deferreturn将此时goroutine上绑定的defer全部执行完毕(deferreturn函数详解可以参考Go defer(一):延迟调用的使用及其底层实现原理详解 )
- 如果遍历完整个defer链表,还没有调用recovery函数,则会调用fatalpanic函数来打印panic函数,并退出程序

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