Go 的秘密生活:'defer' 语句

发布: (2026年2月9日 GMT+8 12:44)
8 分钟阅读
原文: Dev.to

I’m happy to translate the article for you, but I need the actual text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the text, I’ll translate it into Simplified Chinese while preserving the formatting, markdown, and any code blocks.

Source:

第20章:堆叠的牌组

伊桑的台式电脑风扇嗡嗡作响。他盯着终端,终端像破裂的消防栓一样不断抛出错误信息。

panic: too many open files
panic: too many open files

“我不明白,”他嘀咕着,按下 Ctrl+C 杀掉服务器。“我已经把所有东西都关了。我检查了三遍。”

埃莉诺端着一盘茶走过来。“把 ExportData 函数给我看看。”

伊桑打开了代码。那是一个大约 50 行的函数,打开文件、查询数据库、写入 CSV,然后上传到 S3。

func ExportData(id string) error {
    f, err := os.Open("data.csv")
    if err != nil {
        return err
    }

    db, err := sql.Open("postgres", "...")
    if err != nil {
        return err // <--- 泄漏点
    }

    // ... 40 行逻辑 ...

    db.Close()
    f.Close() // <--- 清理
    return nil
}

“我在底部把文件关了,”伊桑指着说。

“如果数据库连接失败会怎样?”埃莉诺温和地问。

伊桑看了看第二个 if err != nil 块。“它会返回错误。”

“那在返回之前会关闭文件吗?”

伊桑僵住了。“不会。它会立即返回。文件保持打开状态。”

“正是如此。如果在那 40 行逻辑的中间出现错误呢?文件仍然保持打开。如果函数 panic?文件仍然保持打开。你已经产生了泄漏。”

“在其他语言里,”埃莉诺解释道,“你可能会把这段代码包在 try...finally 块里。把清理代码放在很远的底部,你必须记得往下滚动去检查它。”

“在 Go 里,我们更倾向于 近距离(Proximity)。”

她拿起键盘,把清理代码移到了更靠近资源创建的位置。

func ExportData(id string) error {
    f, err := os.Open("data.csv")
    if err != nil {
        return err
    }
    defer f.Close() // 立即安排!

    db, err := sql.Open("postgres", "...")
    if err != nil {
        // 此时 f.Close() 会自动执行
        return err
    }
    defer db.Close() // 立即安排!

    // ... 40 行逻辑 ...

    return nil
    // db.Close() 会在这里自动执行
    // f.Close() 会在这里自动执行
}

defer 关键字会把函数调用压入一个栈,”埃莉诺说。“它的意思是:‘不管这个函数怎么结束——返回、错误还是 panic——在离开之前先执行这段代码。’

“所以我把清理代码紧挨着创建代码放好?”

“永远如此。打开门后,立刻告诉门在你离开时自行关闭。你再也不会忘记。它同样适用于文件、数据库连接,尤其是互斥锁。”

mu.Lock()
defer mu.Unlock() // 无论发生什么,锁都会被释放

“等等,”伊桑看着代码说,“我现在有两个 defer。哪个先运行?”

“它是一个栈,”埃莉诺回答。“后进先出。”

她在记事本上画了示意图:

  1. 打开文件 → 压入 f.Close
  2. 打开数据库 → 压入 db.Close
  3. 函数结束 → 弹出 db.Close(先运行) → 弹出 f.Close(后运行)

“这点很关键,”她指出。“想象你在写一个带缓冲的写入器。你需要在 Close 文件之前先 Flush 缓冲区。因为你先创建 File 再创建 Writer,Writer 会先关闭。依赖顺序自然得到处理。”

参数求值的陷阱

“一点提醒,”埃莉诺举起一根手指,“函数调用虽然被安排在以后执行,但参数会在现在求值。”

“这是什么意思?”

“看这个例子。”

func TrackTime() {
    start := time.Now()
    defer fmt.Println("Time elapsed:", time.Since(start))

    time.Sleep(2 * time.Second)
}

伊桑运行了代码。

Time elapsed: 0s

“零?”伊桑问。“但它睡了两秒。”

“因为 time.Since(start) 在你写 defer 那一行时就已经计算了,”埃莉诺解释。“那一刻 start 就是当前时间,所以差值为零。”

修复计时示例

// 这里继续写修正后的代码示例...
func TrackTime() {
    start := time.Now()
    defer func() {
        fmt.Println("Time elapsed:", time.Since(start))
    }()

    time.Sleep(2 * time.Second)
}

再次运行时会输出:

Time elapsed: 2s

现在计算发生在匿名函数内部,且在最后一步执行。

Ethan 看了看他的 ExportData 函数。它看起来更安全、更稳健。

“我以前以为 defer 只是语法糖,”他说。
“它是一个安全网,”Eleanor 纠正道。 “我们是人,会忘事,会分心。defer 让你在进行中就能清理资源,这样永远不会给未来的自己留下烂摊子。”

defer 语句

使用场景

  • 关闭文件 (f.Close())
  • 释放互斥锁 (mu.Unlock())
  • 关闭数据库连接 (db.Close())

执行顺序(后进先出)

  • 被延迟调用会被存入栈中。
  • 当外围函数返回时,最后 被延迟的函数会 最先 执行。

Panic 安全性

  • 即使函数发生 panic,延迟函数仍会执行,提供可靠的清理路径。

参数求值

  • 当执行 defer 语句时,传递给延迟函数的参数会 立即 求值。
  • 函数体则在稍后返回时才运行。

小技巧

如果需要在函数结束时计算某些内容(例如计时),可以将逻辑包装在匿名函数中:

defer func() {
    // computation that should happen just before the function exits
}()

下一章:Panic 与 Recover。Ethan 了解到,有时拯救程序的唯一办法就是让它崩溃(然后捕获它)。

0 浏览
Back to Blog

相关文章

阅读更多 »

Go 模板

什么是 Go 模板?Go 模板是一种在 Go 中通过将数据与纯文本或 HTML 文件混合来创建动态内容的方式。它们允许您替换占位符……