Go 的秘密生活:'defer' 语句
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。哪个先运行?”
“它是一个栈,”埃莉诺回答。“后进先出。”
她在记事本上画了示意图:
- 打开文件 → 压入
f.Close - 打开数据库 → 压入
db.Close - 函数结束 → 弹出
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 了解到,有时拯救程序的唯一办法就是让它崩溃(然后捕获它)。