目录
正在加载目录…
专栏文章
专栏文章
GoLang 专栏
1. GoLang:语法、并发与工程实践 2. GoLang 类型系统:接口、泛型与数据结构 3. 并发编程 4. GoLang 错误处理:error、panic 与 defer 5. GoLang 标准库速查:高频包与实战用法 6. GoLang 工程实践:项目结构、测试与部署

GoLang 错误处理:error、panic 与 defer

发布于 2026-07-06 22:30 · 最后编辑于 2026-07-31 15:51 · 字数 910 👁 81 次阅读

深入 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 接口

../../assets/04 GoLang 错误处理与 panic defer/file 20260605123655342

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

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论