Go 的 defer,简单但有几个地方得留神

举报
yd_241780069 发表于 2026/11/08 20:56:48 2026/11/08
【摘要】 起因defer 是 Go 里我用得最多的关键字之一,开文件、加锁、关连接,基本每个函数里都有。用起来简单,但有几个行为跟直觉不太一样,踩过之后才记住。 执行顺序是倒着的defer fmt.Println("first")defer fmt.Println("second")defer fmt.Println("third")输出是 third、second、first。后进先出,跟栈一样。...

起因

defer 是 Go 里我用得最多的关键字之一,开文件、加锁、关连接,基本每个函数里都有。用起来简单,但有几个行为跟直觉不太一样,踩过之后才记住。

执行顺序是倒着的

defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")

输出是 third、second、first。后进先出,跟栈一样。这个知道的人多,但实际用的时候还是会忘。比如先 defer file.Close() 再 defer tx.Rollback(),回滚在关闭之后执行,顺序就反了。得按执行顺序倒着写 defer。

参数在 defer 那一刻就求值了

x := 1
defer fmt.Println(x) // 输出 1,不是 2
x = 2

这个坑我踩过。defer 后面的函数参数在声明 defer 的时候就求值了,不是执行的时候。想用最终值,得包一层闭包:

defer func() {
    fmt.Println(x) // 输出 2
}()

闭包捕获的是变量本身,不是当时的值。开文件、关连接这种一般不受影响,因为 f 传进去之后不会再变。但涉及循环变量或者后面会改的变量,就得注意。

和命名返回值配合,能改返回值

这个特性知道的人少一点。命名返回值在 defer 里是可以改的:

func do() (err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("do failed: %w", err)
        }
    }()
    return someFunc()
}

return 先把值赋给 err,然后执行 defer,defer 里改了 err,最终返回的是改过的值。用这个可以统一给错误加包装,省得每个 return 都写一遍。

不用命名返回值的话,defer 里改不了返回值,因为返回值已经确定了。

循环里 defer,容易积攒

for _, path := range paths {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // 全部攒到函数结束才关
}

循环里 defer,所有文件句柄都攒到函数返回才关,文件多了可能超限。正确做法是拆成函数,让 defer 在每次迭代结束时就执行:

for _, path := range paths {
    if err := readFile(path); err != nil {
        return err
    }
}

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

或者手动关,不靠 defer。循环里我一般不用 defer,除非循环次数很少。

defer 和 recover,只对当前 goroutine 有效

go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Println("recovered:", r)
        }
    }()
    panic("boom")
}()

recover 只能捕获当前 goroutine 的 panic。主 goroutine 里 defer recover 了,子 goroutine panic 了照样把进程带崩。所以每个可能 panic 的 goroutine 入口都得自己加 defer recover。

我现在的习惯是,起 goroutine 的地方统一包一层:

go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("goroutine panic: %v\n%s", r, debug.Stack())
        }
    }()
    doWork()
}()

堆栈打出来,不然光知道 panic 了,不知道在哪。

defer 有性能开销,但一般不用管

Go 1.14 之后 defer 做了优化,大部分场景下开销很小,比函数调用多不了多少。只有在极热的循环里,defer 的开销才可能被放大。这种场景本来就该避免在循环里 defer,所以正常写代码不用为了性能刻意不用 defer。

我之前看到有人说“defer 慢,别用”,那是老黄历了。现在该用就用,可读性比那点开销重要。

一个容易忽略的:defer 在 return 之后执行

func do() int {
    x := 1
    defer func() { x = 2 }()
    return x // 返回 1
}

return x 先把 x 的值(1)赋给返回值,然后执行 defer,defer 里改了 x,但返回值已经是 1 了。除非返回值是命名的,否则 defer 改不了。

这个和前面命名返回值那条是一体两面。理解的关键是:return 不是原子操作,它分两步——赋值返回值,执行 defer。

小结

defer 简单,但几个行为得记牢:倒序执行、参数当场求值、命名返回值能改、循环里别攒、recover 只管当前 goroutine。日常写代码该用就用,可读性优先,别为了那点性能把代码写拧巴。

先记这些。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。