defer 是後進先出,登記時機在執行到那一行
defer把一個呼叫延到所在函式結束時才執行。三個容易記錯的細節:後進先出、執行到那一行才登記、函式結束包含 panic。
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// 輸出:3 2 1一、後進先出,不是依序
像堆疊。這個順序是刻意的:後開的資源先關,正好對應「開啟順序的反向」。
二、執行到那一行才登記
defer 不是「掃描整個函式」,而是跑到那一行時才把呼叫壓進堆疊。所以提早 return 的話,後面的 defer 根本沒被登記過:
func f() {
defer fmt.Println("A") // 有登記
return
defer fmt.Println("B") // 永遠不會登記
}參數也是登記當下就求值,不是執行時才算。
三、「函式結束」包含 panic
正常 return、提早 return、panic 展開——三種情況 defer 都會跑。這正是它被拿來做資源釋放(defer f.Close())與 recover 的原因,本質上等同其他語言的 try / finally。
⚠️ 作用域是「函式」不是「區塊」
迴圈裡的 defer 不會在每圈結束時執行,而是全部堆到函式結束才一次倒序跑完:
for i := 0; i < 3; i++ {
defer fmt.Println(i) // 不是每圈跑一次
}
// 函式結束時才輸出:2 1 0在迴圈裡開檔案並 defer file.Close() 是經典的 file descriptor 洩漏——要嘛把迴圈主體抽成一個函式,要嘛不要用 defer。
這算不算像 goto
不算。goto 的目的地不確定,defer 的目的地永遠固定是函式結尾——所以讀 code 時只有一條規則要記,不是每次都要追。真正需要小心的只有上面那條「作用域是函式不是區塊」。