Go 部落格

Defer、Panic 和 Recover

Andrew Gerrand
2010 年 8 月 4 日

Go 具有常用的控制流機制:if、for、switch、goto。它還具有 go 語句,用於在單獨的 goroutine 中執行程式碼。在這裡,我想討論一些不太常見的機制:defer、panic 和 recover。

defer 語句將函式呼叫推送到一個列表。儲存的呼叫列表在周圍函式返回後執行。Defer 通常用於簡化執行各種清理操作的函式。

例如,讓我們看一個開啟兩個檔案並將一個檔案內容複製到另一個檔案的函式

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

這可以工作,但有一個 bug。如果對 os.Create 的呼叫失敗,函式將在不關閉原始檔的情況下返回。這可以透過在第二個 return 語句之前放置對 src.Close 的呼叫輕鬆補救,但如果函式更復雜,問題可能不容易被發現和解決。透過引入 defer 語句,我們可以確保檔案始終關閉

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

defer 語句允許我們在開啟每個檔案後立即考慮關閉它,保證無論函式中有多少 return 語句,檔案都被關閉。

defer 語句的行為是直接且可預測的。有三個簡單的規則

  1. 延遲函式的引數在 defer 語句被評估時評估。

在此示例中,表示式“i”在 Println 呼叫被延遲時進行評估。延遲的呼叫將在函式返回後列印“0”。

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
  1. 延遲函式呼叫在周圍函式返回後以“後進先出”的順序執行。

此函式列印“3210”

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
  1. 延遲函式可以讀取並分配給返回函式的命名返回值。

在此示例中,延遲函式在周圍函式返回遞增返回值 i。因此,此函式返回 2

func c() (i int) {
    defer func() { i++ }()
    return 1
}

這對於修改函式的錯誤返回值很方便;我們很快就會看到一個例子。

Panic 是一個內建函式,它停止正常的控制流並開始panic。當函式 F 呼叫 panic 時,F 的執行停止,F 中的任何延遲函式正常執行,然後 F 返回到其呼叫者。對於呼叫者,F 表現得像一個對 panic 的呼叫。這個過程沿著堆疊繼續,直到當前 goroutine 中的所有函式都已返回,此時程式崩潰。Panic 可以透過直接呼叫 panic 來啟動。它們也可以由執行時錯誤引起,例如陣列越界訪問。

Recover 是一個內建函式,它重新獲得對 panicking goroutine 的控制。Recover 僅在延遲函式內部有用。在正常執行期間,對 recover 的呼叫將返回 nil 並且沒有其他效果。如果當前 goroutine 正在 panicking,對 recover 的呼叫將捕獲給定給 panic 的值並恢復正常執行。

這是一個示例程式,演示了 panic 和 defer 的機制

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

函式 g 接受 int i,如果 i 大於 3 則 panic,否則它以引數 i+1 呼叫自身。函式 f 延遲一個呼叫 recover 並列印恢復值(如果它不為 nil)的函式。在繼續閱讀之前,請嘗試想象一下這個程式的輸出可能是什麼。

程式將輸出

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

如果我們將延遲函式從 f 中移除,panic 將不會被恢復併到達 goroutine 呼叫堆疊的頂部,從而終止程式。這個修改後的程式將輸出

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4

panic PC=0x2a9cd8
[stack trace omitted]

有關 panicrecover 的實際示例,請參閱 Go 標準庫中的 json 包。它使用一組遞迴函式編碼介面。如果在遍歷值時發生錯誤,則呼叫 panic 以將堆疊展開到頂層函式呼叫,該呼叫從 panic 中恢復並返回適當的錯誤值(請參閱 encode.go 中 encodeState 型別的“error”和“marshal”方法)。

Go 庫中的約定是,即使包在內部使用 panic,其外部 API 仍然提供顯式錯誤返回值。

defer 的其他用途(除了前面給出的 file.Close 示例)包括釋放互斥鎖

mu.Lock()
defer mu.Unlock()

列印頁尾

printHeader()
defer printFooter()

等等。

總而言之,defer 語句(無論是否帶有 panic 和 recover)提供了一種不尋常而強大的控制流機制。它可用於模擬其他程式語言中由專用結構實現的一些功能。試試看吧。

下一篇文章:Go 榮獲 2010 年 Bossie 獎
上一篇文章:透過通訊共享記憶體
部落格索引