【譯】defer-panic-and-recover

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


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

Tags: