首页 / Go 语言入门教程 / defer 详解

Go 语言入门教程

defer 详解

本教程共 80 篇 · 第 25 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程deferLIFO资源释放Go 1.14Go defer

25. defer 详解

本节目标:彻底搞懂 defer 的执行顺序、参数求值时机,以及 Go 1.14+ 的性能优化,学会用 defer 管理资源。

什么是 defer

defer 语句用于延迟执行一个函数调用,被 defer 的函数会在当前函数返回之前执行。

最经典的用途是资源释放:

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()  // 确保函数返回时关闭文件

    // 读取文件内容...
    return nil
}

不管函数从哪个 return 返回,甚至 panic 了,f.Close() 都会被执行。

defer 的执行顺序

多个 defer 按后进先出(LIFO)顺序执行,就像栈一样:

func main() {
    defer fmt.Println("第一个")
    defer fmt.Println("第二个")
    defer fmt.Println("第三个")
    fmt.Println("正常执行")
}

输出:

正常执行
第三个
第二个
第一个

先注册的最后执行,后注册的最先执行。

为什么是 LIFO

因为资源的获取和释放通常是嵌套的。先打开的文件最后关闭,先获取的锁最后释放:

func process() {
    f1, _ := os.Open("file1")
    defer f1.Close()

    f2, _ := os.Open("file2")
    defer f2.Close()

    // f2 先关闭,f1 后关闭
}

LIFO 保证了资源的正确释放顺序。

参数立即求值

defer 的参数在 defer 语句执行时就求值,不是在函数真正执行时。

看个例子

func main() {
    x := 10

    defer fmt.Println("defer 中 x =", x)  // 此时 x = 10

    x = 20
    fmt.Println("正常 x =", x)
}

输出:

正常 x = 20
defer x = 10

defer fmt.Println("defer 中 x =", x) 执行时,x 是 10,这个值就被「固定」了。后面 x = 20 不影响 defer 的参数。

闭包方式延迟求值

如果你想让 defer 在执行时才读取变量的值,用闭包:

func main() {
    x := 10

    defer func() {
        fmt.Println("defer 中 x =", x)  // 执行时读取 x
    }()

    x = 20
    fmt.Println("正常 x =", x)
}

输出:

正常 x = 20
defer x = 20

因为 defer 注册的是匿名函数,匿名函数没有参数,x 是在执行时从闭包捕获的。

Tip

记住这个区别:

  • defer fmt.Println(x) — 参数立即求值
  • defer func() { fmt.Println(x) }() — 闭包延迟求值

defer 和返回值

defer 执行时机是 return 之后、函数真正退出之前。它可以修改命名返回值:

func foo() (result int) {
    defer func() {
        result *= 2  // 修改返回值
    }()

    return 5
}

func main() {
    fmt.Println(foo())  // 10
}

执行流程:

  1. return 5 把 5 赋给 result
  2. 执行 defer,result *= 2 变成 10
  3. 函数返回 result,值是 10
Note

只有命名返回值能被 defer 修改。匿名返回值不行:

func bar() int {
    result := 5
    defer func() {
        result *= 2  // 修改的是局部变量,不影响返回值
    }()
    return result  // 返回 5,不是 10
}

defer 常见用途

资源释放

func readFile(path string) {
    f, err := os.Open(path)
    if err != nil {
        return
    }
    defer f.Close()  // 确保关闭

    // 使用文件...
}

锁释放

var mu sync.Mutex

func safeUpdate(m map[string]int, key string, value int) {
    mu.Lock()
    defer mu.Unlock()  // 确保解锁

    m[key] = value
}

恢复 panic

func safeRun() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到 panic:", r)
        }
    }()

    panic("出错了")
}

计时器

func timeTrack(start time.Time, name string) {
    elapsed := time.Since(start)
    fmt.Printf("%s 耗时 %v\n", name, elapsed)
}

func slowFunc() {
    defer timeTrack(time.Now(), "slowFunc")

    time.Sleep(2 * time.Second)
}

日志记录

func handleRequest(req string) {
    defer log.Printf("请求处理完成:%s", req)
    log.Printf("开始处理请求:%s", req)

    // 处理请求...
}

Go 1.14+ defer 优化

在 Go 1.14 之前,每个 defer 都有一定的性能开销(大约几十纳秒),因为它需要在堆上分配 defer 记录。

Go 1.14 引入了开放编码(Open-coded defer)优化:

  • 对于最多 8 个 defer 的函数
  • 且 defer 不在循环内
  • 编译器会把 defer 内联,几乎零开销

优化后的 defer 性能和直接调用函数差不多:

// Go 1.14+ 优化后,这些 defer 几乎零开销
func fastFunc() {
    mu.Lock()
    defer mu.Unlock()

    f, _ := os.Open("file")
    defer f.Close()

    // 快速操作...
}
Tip

Go 1.14 以后,大部分场景下你不需要担心 defer 的性能开销。优先用 defer 保证资源释放的正确性,而不是为了「性能」手动管理。

defer 的注意事项

defer 在循环中

// 不推荐:defer 在循环中,资源会堆积到函数返回才释放
func processFiles(paths []string) {
    for _, path := range paths {
        f, _ := os.Open(path)
        defer f.Close()  // 所有文件在函数返回时才关闭

        // 处理文件...
    }
}

更好的做法是把循环体提取成函数:

func processFiles(paths []string) {
    for _, path := range paths {
        processFile(path)
    }
}

func processFile(path string) {
    f, err := os.Open(path)
    if err != nil {
        return
    }
    defer f.Close()  // 每次调用结束后就关闭

    // 处理文件...
}

defer 的参数是函数调用

// defer 执行的是 fmt.Println 的返回值?不,defer 延迟执行整个函数调用
defer fmt.Println("done")

// 等价于:在函数返回前执行 fmt.Println("done")

defer 后面必须跟函数调用,不能是表达式:

defer x++  // 报错:defer requires a function call
defer func() { x++ }()  // 正确

defer 不能在 return 语句之后

func foo() int {
    return 1
    defer fmt.Println("不会执行")  // 永远不会执行,因为前面已经 return 了
}

defer 必须在 return 之前注册才能执行。

实际示例

数据库事务

func updateUser(db *sql.DB, id int, name string) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }

    // 根据是否出错决定提交还是回滚
    success := false
    defer func() {
        if success {
            tx.Commit()
        } else {
            tx.Rollback()
        }
    }()

    _, err = tx.Exec("UPDATE users SET name = ? WHERE id = ?", name, id)
    if err != nil {
        return err  // 触发 Rollback
    }

    success = true  // 标记成功,触发 Commit
    return nil
}

HTTP 响应体关闭

func fetchURL(url string) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()  // 确保关闭响应体

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }

    return string(body), nil
}
Warning

HTTP 响应体必须关闭,否则会导致连接泄漏。用 defer 是最安全的方式。

常见问题

defer 执行顺序搞错

func main() {
    defer fmt.Println("A")
    fmt.Println("B")
    defer fmt.Println("C")
}
// 输出:B C A

defer 不是在它声明的位置执行,而是在函数返回前。多个 defer 按 LIFO 顺序。

defer 修改不了匿名返回值

func foo() int {
    defer func() {
        // 没法修改返回值,因为没有命名
    }()
    return 42
}

想用 defer 修改返回值,必须用命名返回值。

defer 和 goroutine

func main() {
    defer fmt.Println("main 结束")

    go func() {
        defer fmt.Println("goroutine 结束")
        time.Sleep(time.Second)
    }()

    time.Sleep(500 * time.Millisecond)
    // main 先结束,goroutine 可能来不及执行 defer
}

主函数的 defer 在主函数返回时执行,但 goroutine 的 defer 只在 goroutine 自己退出时执行。如果主函数先退出,goroutine 的 defer 可能不会执行。

学完这节你能做什么

  • 用 defer 保证资源释放
  • 理解 defer 的 LIFO 执行顺序
  • 知道 defer 参数立即求值 vs 闭包延迟求值的区别
  • 用 defer 修改命名返回值
  • 了解 Go 1.14+ 的 defer 性能优化

下一节讲 panic 和 recover。