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 時只有一條規則要記,不是每次都要追。真正需要小心的只有上面那條「作用域是函式不是區塊」。

相關:Go 函式的多回傳值與具名回傳值