原文地址:Defer, Panic, and Recover
Go有普通的流程控制機制:if
, for
, switch
, goto
等。它還有Go特有的語句來在單獨的goroutine中執行程序。在這裏我想討論一些不太常見的特性:defer
, panic
和recover
。
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語句的行爲是直接和可預測的。有三條簡單的規則:
defer函數的參數在評估函數的時候被評估(通俗的講就是,被defer函數的實參值就是執行defer語句時該變量的值)。
在下面的實例中,在執行defer語句將函數
fmt.Println(i)
壓入defer list時,變量i的值爲0,所以被defer的函數實際執行時,它的實參爲0。函數最終打印的是0func a() { i := 0 defer fmt.Println(i) i++ return }
在執行defer的外圍函數返回後,被defer的函數會按後入先出(LIFO)的順序執行。下面的函數將打印”3210”。
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
被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]
對於一個真實的panic和recover的示例,可以參考Go標準庫的json package。它使用一組遞歸函數解碼JSON編碼的數據。當遇到格式錯誤的JSON時,解析器將調用panic來展開到頂層函數的調用棧,頂層函數將使用recover從panic中恢復控制權並返回一個合適的錯誤(參考decode.go中decodeState
的error
和unmarshal
方法。
Go語言庫的一個慣例就是,即使一個包在內部使用了panic,它對外暴露的API仍然會呈現明確的錯誤返回值。
除了開篇示例的關閉文件外,defer還常被用於:釋放互斥鎖。
mu.Lock()
defer mu.Unlock()
打印頁腳,等等很多。
printHeader()
defer printFooter()
summary
defer語句(不管是否與panic和recover一起)爲控制流提供了一個不尋常的且強大的機制。它可以用來模擬很多其他語言專門設計的數據結構實現的功能。試一下。