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
}

這樣做可以,但是有一個錯誤。如果呼叫 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. 延遲函式呼叫在外部函式返回後以 Last In First Out (後進先出) 的順序執行。

這個函式會列印“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 是一個內建函式,它會停止正常的控制流並開始恐慌。當函式 F 呼叫 panic 時,F 的執行停止,F 中的任何延遲函式正常執行,然後 F 返回給它的呼叫者。對於呼叫者來說,F 的行為就像呼叫了 panic。這個過程會一直向上沿著呼叫棧傳播,直到當前 goroutine 中的所有函式都返回,此時程式崩潰。Panic 可以透過直接呼叫 panic 觸發。它們也可以由執行時錯誤引起,例如陣列越界訪問。

Recover 是一個內建函式,用於恢復正在恐慌的 goroutine 的控制。Recover 僅在延遲函式內部有用。在正常執行期間,呼叫 recover 會返回 nil 並且沒有其他效果。如果當前 goroutine 正在恐慌,呼叫 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 獎
上一篇文章:透過通訊共享記憶體
部落格索引