[譯]defer, panic, recover

原文地址:Defer, Panic, and Recover

Go有普通的流程控制機制:if, for, switch, goto等。它還有Go特有的語句來在單獨的goroutine中執行程序。在這裏我想討論一些不太常見的特性:defer, panicrecover

defer

defer語句將一個函數調用壓入到一個列表中。列表中存儲的函數調用,將在執行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。bug就是在os.Create(dstName)失敗時,函數將直接返回,而不會關閉已經打開的源文件。通過在err != nil成立時的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語句允許我們在打開文件後就立即考慮關閉文件的事情,保證不管函數有多少返回點文件都將被正確關閉。

defer語句的行爲是直接和可預測的。有三條簡單的規則:

  1. defer函數的參數在評估函數的時候被評估(通俗的講就是,被defer函數的實參值就是執行defer語句時該變量的值)。

    在下面的實例中,在執行defer語句將函數fmt.Println(i)壓入defer list時,變量i的值爲0,所以被defer的函數實際執行時,它的實參爲0。函數最終打印的是0

    func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
    }
  2. 在執行defer的外圍函數返回後,被defer的函數會按後入先出(LIFO)的順序執行。下面的函數將打印”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

panic是一個內建函數,用於停止執行常規控制流程,並開始安全退出。當一個函數F調用了panic後,F將停止執行,任何F中的被defer開始被正常執行,然後F將控制權返回給它的調用者。對於F的調用者來說,這時F的行爲就像是一次panic調用一樣。這個過程將沿着棧繼續往上,直到當前goroutine中的所有函數都返回,此時程序將崩潰。Panics可以通過直接調用panic函數發起。它們也可能由運行時錯誤引起,比如數組訪問越界等。

recover

recover也是一個內建函數,它用於重新獲取一個panicking goroutine的控制權。recover只有在被defer的函數中才有用。在正常執行期間,調用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接收一個整型的傳參i,並且如果實參i大於3就會panic,如果不大於3則使用參數i+1遞歸調用自己。函數f defer了一個函數,該函數調用recover來捕獲panic值,如果捕獲到的值不是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中的defer函數,則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 package。它使用一組遞歸函數解碼JSON編碼的數據。當遇到格式錯誤的JSON時,解析器將調用panic來展開到頂層函數的調用棧,頂層函數將使用recover從panic中恢復控制權並返回一個合適的錯誤(參考decode.godecodeStateerrorunmarshal方法。

Go語言庫的一個慣例就是,即使一個包在內部使用了panic,它對外暴露的API仍然會呈現明確的錯誤返回值。

除了開篇示例的關閉文件外,defer還常被用於:釋放互斥鎖。

mu.Lock()
defer mu.Unlock()

打印頁腳,等等很多。

printHeader()
defer printFooter()

summary

defer語句(不管是否與panicrecover一起)爲控制流提供了一個不尋常的且強大的機制。它可以用來模擬很多其他語言專門設計的數據結構實現的功能。試一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章