GoLang 错误处理:error、panic 与 defer
深入 Go 的错误处理哲学:error 接口的设计与包装、panic/recover 的执行模型,以及 defer 语句的调用时机与求值规则。
相关文章:GoLang 简介 · GoLang 类型系统与数据结构深度 · GoLang 并发编程 · GoLang 标准库速查 · GoLang 工程实践
目录
| 章节 | 说明 |
|---|---|
| error 接口 | 标准错误模式、哨兵错误、自定义错误类型 |
| 错误包装与判断 | errors.Is、errors.As、fmt.Errorf %w |
| panic 与 recover | 触发场景、recover 的正确用法 |
| defer 执行规则 | 调用时机、参数求值、执行顺序 |
| defer + panic + recover 协作 | 完整执行模型 |
error 接口
Go 的错误处理基于接口而非异常:
// error 是一个内置接口
type error interface {
Error() string
}
标准错误处理模式
// 函数返回 (结果, error),error 排最后
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// 调用方立即检查错误
result, err := divide(10, 0)
if err != nil {
// 处理错误
log.Fatal(err)
}
哨兵错误(Sentinel Error)
// 预定义的、可以直接比较的错误值
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
)
// 调用方判断
if err == ErrNotFound {
// 处理"未找到"
}
// Go 1.13+ 推荐用 errors.Is(支持错误包装链)
if errors.Is(err, ErrNotFound) {
// 更安全,能穿透 Wrapped error
}
自定义错误类型
当需要在错误中携带额外信息时,定义错误类型:
// 实现 error 接口
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
// 暴露内部 Err 以支持 errors.As/errors.Is 穿透
func (e *PathError) Unwrap() error {
return e.Err
}
错误包装与判断
Go 1.13 引入错误包装机制,构建错误链:
// 包装错误(%w 动词)
err := fmt.Errorf("database query failed: %w", originalErr)
// errors.Is:判断错误链中是否含有目标错误(按值比较)
if errors.Is(err, sql.ErrNoRows) {
// err 或其任意包装层是 sql.ErrNoRows
}
// errors.As:提取错误链中第一个匹配类型的错误(按类型匹配)
var pathErr *PathError
if errors.As(err, &pathErr) {
fmt.Println("failed path:", pathErr.Path)
}
== vs errors.Is 的区别:
wrapped := fmt.Errorf("context: %w", ErrNotFound)
wrapped == ErrNotFound // false(包装后不相等)
errors.Is(wrapped, ErrNotFound) // true(穿透包装层)
panic 与 recover
panic 的触发场景
// 1. 手动触发
panic("something went wrong")
panic(errors.New("critical error"))
// 2. 运行时自动触发(常见)
var s []int
_ = s[0] // index out of range
var p *int
_ = *p // nil pointer dereference
var m map[string]int
m["key"] = 1 // assignment to entry in nil map
ch := make(chan int)
close(ch)
ch <- 1 // send on closed channel
panic 的扩散
panic 在当前 goroutine 中从触发点开始"向上"传播
→ 当前函数停止执行
→ 逐层执行已注册的 defer(每层都执行)
→ 到达 goroutine 顶部 → 程序崩溃并打印 stack trace
recover
recover() 必须在 defer 函数内直接调用才有效:
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered panic: %v", r)
}
}()
return a / b, nil // b=0 时会 panic
}
// ❌ 无效:recover 不在 defer 函数的直接调用中
defer recover() // 无效
defer func() { recover() }() // ✅ 有效
recover 的限制:
- 只能捕获当前 goroutine 的 panic
- 子 goroutine 的 panic 无法被父 goroutine 的 recover 捕获
// ❌ 无法捕获子 goroutine 的 panic
func main() {
defer func() {
recover() // 无效!
}()
go func() {
panic("child goroutine panic") // 程序仍会崩溃
}()
time.Sleep(time.Second)
}
// ✅ 每个 goroutine 自己处理自己的 panic
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("recovered:", r)
}
}()
// 可能 panic 的代码
}()
defer 执行规则
调用时机
defer 注册的函数在当前函数返回时执行(包括正常 return 和 panic):
func f() {
defer fmt.Println("1") // 第三个执行
defer fmt.Println("2") // 第二个执行
defer fmt.Println("3") // 第一个执行
// defer 是 LIFO(后进先出)
}
// 输出:3 2 1
参数立即求值
defer 调用的函数参数在 defer 语句执行时立即求值,不是在函数返回时:
func f() {
x := 1
defer fmt.Println(x) // x 在 defer 语句时求值 = 1
x = 2
// 函数返回时打印 1,而不是 2
}
// 若要在返回时获取最新值,用闭包
func g() {
x := 1
defer func() {
fmt.Println(x) // 闭包捕获 x 的引用,打印 2
}()
x = 2
}
defer 修改命名返回值
defer 可以修改命名返回值:
func double(x int) (result int) {
defer func() {
result *= 2 // 修改命名返回值
}()
result = x
return // 返回 x*2,不是 x
}
fmt.Println(double(5)) // 输出 10
defer 与循环
在循环中使用 defer 时要注意资源释放时机:
// ❌ 所有 defer 积累到函数返回时才执行,文件可能耗尽
for _, file := range files {
f, _ := os.Open(file)
defer f.Close() // 不在每次迭代结束时关闭
}
// ✅ 用匿名函数封装,每次迭代结束时关闭
for _, file := range files {
func() {
f, _ := os.Open(file)
defer f.Close()
}()
}
defer + panic + recover 协作
完整执行模型:
函数执行
→ 遇到 defer → 注册到 defer 栈
→ 遇到 panic
→ 停止当前函数继续执行
→ 按 LIFO 顺序执行 defer 栈中的函数
→ 若某个 defer 中调用了 recover()
→ recover 捕获 panic 值
→ 该 defer 函数返回后,panic 停止传播
→ 当前函数返回(零值)
→ 若没有 recover
→ 传播到上层调用方,继续执行上层的 defer 栈
→ 直到 goroutine 顶部 → 程序崩溃
func example() {
defer func() {
fmt.Println("defer 3") // 第一个执行(LIFO)
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
defer fmt.Println("defer 2") // 第二个执行
defer fmt.Println("defer 1") // 第三个执行... 等等
// 实际 LIFO 顺序:defer 1 先注册但最后... 不,让我重新说明
// defer 注册顺序:defer 1 → defer 2 → defer 3
// 执行顺序(LIFO):defer 3 → defer 2 → defer 1
panic("test panic")
}
实际顺序演示:
func demoOrder() {
defer fmt.Println("A") // 最后执行
defer fmt.Println("B") // 中间
defer fmt.Println("C") // 最先执行(LIFO)
}
// 输出:C → B → A
panic 与 defer 的协作示例:
func main() {
fmt.Println("start")
defer fmt.Println("main defer") // panic 后也会执行
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("inner panic")
fmt.Println("never printed")
}()
fmt.Println("after inner func") // panic 被 recover 后,这里会执行
}
// 输出:
// start
// recovered: inner panic
// after inner func
// main defer
参考资料
- 《Go 语言核心 36 讲》— 第 19-22 讲(郝林)
- Go Blog: Error handling and Go
- Go Blog: Working with Errors in Go 1.13
- Defer, Panic, and Recover
评论 (0)