【譯】defer-panic-and-recover

狼爺發表於2022-05-01

Go 有通用的控制流程:if,for,switch,goto。它也有go語句用於讓程式碼執行在單獨的協程。這裡我將討論一些不常見的問題: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被宣告時,其引數就會被計算。

在此示例中,當Println呼叫被defer宣告,將計算表示式“i”。defer呼叫將在函式返回後列印“0”。

2、defer的執行順序為先進後出。

此函式列印“3210”:

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

3、defer可以讀取有名返回值。
在此示例中,defer函式在函式返回後遞增返回值i。因此,此函式返回2:

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

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

Panic是一個內建功能,可以停止普通的控制流程並開始Panicing。當函式F呼叫panic時,F的執行會停止,F中的任何defer函式都正常執行,然後F返回給其呼叫方。對於呼叫方,F的表現就像是panic。該過程繼續在堆疊中向上移動,直到當前協程 中的所有函式都返回,此時程式崩潰。可以通過直接呼叫panic來啟動panic。它們也可能是由執行時錯誤引起的,例如越界陣列訪問。

recover是一個內建功能,可以重新獲得對正在panic的協程的控制。recover僅在defer函式中有用。在正常執行期間,recover呼叫將返回nil,並且沒有其他效果。如果當前協程 出現panic,則呼叫recover將捕獲panic提供的值並恢復正常執行。

下面是一個示例程式,演示了panicdefer的機制:

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 defer呼叫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中刪除derfer函式,則不會恢復panic,並在到達協程呼叫堆疊頂部時終止程式。這個修改後的程式將輸出:

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語句(有/沒有panicrecover)為控制流提供了一種不同尋常且功能強大的機制。它可用於對其他程式語言中特殊用途結構實現的許多特性進行建模。試試吧。


原文 https://golang.google.cn/blog/defer-panic-and-recover

相關文章