Files
cs-note/hzh/GolangStar/Go语言基础/Go语言defer.md
T

4.1 KiB
Raw Blame History

tags, create time
tags create time
go
golang
go基础语法
defer
2026-06-07 15:00

Go 语言 defer

概述

defer 是 Go 最优雅的特性之一——延迟函数执行直到外层函数返回。本文解释 defer 的执行顺序、参数求值时机,以及与 return 的交互关系。

正文

为什么 Go 需要 defer?

在 C++ 中你用 RAII(析构函数)管理资源,在 Java/Python 中有 try-finally。Go 选择了更简洁的方案:defer。它让资源清理代码紧跟在资源申请代码后面,而不是藏在函数的末尾。

[!question] ❓ 思考 如果一个函数有多个返回路径(多个 return),如何保证每个路径都正确释放资源?

defer 的基本用法

func main() {
    defer fmt.Println("第三行输出")
    defer fmt.Println("第二行输出")
    fmt.Println("第一行输出")
}
// 输出:
// 第一行输出
// 第二行输出
// 第三行输出

[!note] 📝 LIFO 执行顺序 defer 栈遵循 后进先出(Last In First Out)。最后一个 defer 最先执行。这类似于调用栈的行为。

核心场景:资源释放

这是 defer 最常见的用途:

func CopyFile(dst, src string) (int64, error) {
    s, err := os.Open(src)
    if err != nil {
        return 0, err
    }
    defer s.Close()  // 无论多少条 return,s 都会被关闭

    d, err := os.Create(dst)
    if err != nil {
        return 0, err
    }
    defer d.Close()  // 同理

    return io.Copy(d, s)
}

如果没有 defer,你需要在每个 return 前手动 Close,代码容易遗漏且冗长。

[!tip] 💡 defer 的最佳实践

  • 打开资源后立即 defer 关闭
  • 不要过度使用——仅在真正需要"函数退出时清理"的场景用
  • 常见场景:文件关闭、锁释放、数据库连接归还

⚠️ defer 的三个经典陷阱

陷阱 1:参数在 defer 声明时就求值

num := 1
defer fmt.Printf("num is %d\n", num)
num = 2
// 输出:num is 1

原因:defer 的参数在声明语句执行时就已经确定并压入栈中了。后续修改 num 不影响已缓存的参数值。

arr := [4]int{1, 2, 3, 4}
defer printArr(&arr)  // 传入的是地址(指针值已确定)
arr[0] = 100
// 输出:100 2 3 4 — 地址没变,但指向的内容变了

[!warning] ⚠️ 值 vs 指针

  • 传值 → 参数在 defer 声明时固化
  • 传指针 → 指针值固化,但指向的内容可变

陷阱 2:return 不是原子操作

Go 的 return 实际上分三步执行:

graph LR
    A["return 语句"] --> B["1. 设置返回值变量"]
    B --> C["2. 执行 defer 栈"]
    C --> D["3. 返回结果"]

这意味着:如果使用了命名返回值,defer 可以修改返回值。

func foo() (res int) {
    defer func() { res++ }()
    return 1
}
fmt.Println(foo())  // 2 — defer 把 res 从 1 改成了 2

但如果返回值是匿名的:

func bar() int {
    defer func() {}()
    return 1
}
fmt.Println(bar())  // 1 — defer 无法影响匿名返回值

陷阱 3:nil panic 不会触发 defer 中的 recover

如果 defer 本身导致 panic(比如 nil 指针解引用),recover 可能捕获不到。确保 defer 体内的代码是安全的。

defer + recover:异常捕获

配合 recover() 可以捕获 panic 并恢复程序:

func safeDivide(a, b int) (result int, recovered string) {
    defer func() {
        if r := recover(); r != nil {
            recovered = fmt.Sprintf("panic: %v", r)
        }
    }()
    result = a / b
    return
}

_, msg := safeDivide(10, 0)
fmt.Println(msg)  // panic: runtime error: integer divide by zero

[!info] ℹ️ recover 的限制

  • recover() 只能在 defer 中调用才有效
  • 它只能恢复同一个 goroutine 中的 panic
  • 不能恢复跨 goroutine 的 panic

关键要点总结

规则 说明
执行时机 外层函数 return 或 panic 时执行
执行顺序 LIFO(后进先出)
参数求值 defer 声明时立即求值
可修改返回值 仅当使用命名返回值时
适用场景 资源清理、日志记录、性能监控