
簡書不維護了,歡迎關注我的知乎:波羅學的個人主頁
翻譯自:https://blog.golang.org/defer-panic-and-recover
Go有和其他語言一樣常見的流程控制語句:if, for, switch, goto。同時也有go表達式來實現(xiàn)在不同的goroutine中運行代碼(并發(fā))。而今天我們將討論的是go的異常控制流程:defer、panic和recover。
Defer
defer語句會將函數(shù)推入到一個列表中。同時列表中的函數(shù)會在return語句執(zhí)行后被調用。defer常常會被用來簡化資源清理釋放之類的操作。
舉個例子,我們來觀察下下面這個函數(shù),它的主要功能是打開兩個文件并將一個文件的內容拷貝到另一個文件:
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
}
該函數(shù)是可用的,但是這里有一個bug。假設我們在調用os.Create時出現(xiàn)了失敗的情況,那么該函數(shù)將會在沒有關閉源文件的情況下立即返回。此問題可以很容易地通過在第二個return語句前調用src.Close來補救。但如果函數(shù)的功能特別復雜,該問題就可能不是那么容易被發(fā)現(xiàn)和解決了。下面介紹一下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語句,便可實現(xiàn)資源的正確釋放。
Defer語句的行為是明確可知的,此處有三條簡單的規(guī)則:
- 函數(shù)參數(shù)值由defer語句調用時確定
比如下面這個例子,打印出來的變量i的值即是運行到defer語句時的值。在a函數(shù)執(zhí)行return后,Defer后的函數(shù)調用,即Println,將會打印出 "0"。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
- deferred的函數(shù)將會在return語句之后按照先進后出的次序執(zhí)行,即LIFO。
下面這個函數(shù)的執(zhí)行結果是 "3210"
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
- deferred函數(shù)還可以讀取return返回值并改變其值。
在下面的例子中,deferred函數(shù)中對返回值進行了自增操作,最終函數(shù)c的最終返回值是2.
func c() (i int) {
defer func() { i++ }()
return 1
}
這使我們可以非常方便的修改異常的函數(shù)返回。
Panic
panic是go的內置函數(shù),它可以終止程序的正常執(zhí)行流程并發(fā)出panic(類似其他語言的exception)。比如當函數(shù)F調用panic,f的執(zhí)行將被終止,然后defer的函數(shù)正常執(zhí)行完后返回給調用者。對調用者而言,F(xiàn)的表現(xiàn)就像調用者直接調用了panic。這個流程會棧的調用次序不斷向上拋出panic,直到返回到goroutine棧頂,此時,程序將會崩潰退出。panic可以通過直接調用panic產(chǎn)生。同時也可能由運行時的錯誤所產(chǎn)生,例如數(shù)組越界訪問。
Recover
recover是go語言的內置函數(shù),它的主要作用是可以從panic的重新奪回goroutine的控制權。Recover必須通過defer來運行。在正常的執(zhí)行流程中,調用recover將會返回nil且沒有什么其他的影響。但是如果當前的goroutine產(chǎn)生了panic,recover將會捕獲到panic拋出的信息,同時恢復其正常的執(zhí)行流程。
下面這個例子向我們展示了panic、defer和recover的執(zhí)行流程。
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)
}
函數(shù)g接收參數(shù)i,如果i大于3就會產(chǎn)生panic,否則調用g(i+1)。而函數(shù)f通過defer匿名函數(shù)來執(zhí)行recover并打印出捕獲到的panic信息(如r不等于nil)。在閱讀代碼前,可嘗試打印下程序輸出。
輸出如下:
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中的recover,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]
下面我們來看一個真實的案例,來自go標準庫的json包。它先通過一系列的遞歸函數(shù)解析json數(shù)據(jù)。當遇到非法json時,解釋器就會產(chǎn)生panic,直到上層調用從panic中重新recover執(zhí)行流程,并據(jù)此返回適當錯誤(具體可以參看decode.go文件中的decodeState的error和unmarshal方法)。
在go的庫中的常見用法是,即使在包內部使用panic,但外部API仍然需要以清晰的error來返回錯誤信息。
下面是defer其他的一些使用場景(除了前面列出的file.close案例),例如鎖的釋放:
mu.Lock()
defer mu.Unlock()
打印頁尾:
printHeader()
defer printFooter()
and more.
總的來說,defer為我們提供了一種異常強大的流程控制機制(不僅僅限于panic、recover場景)。而且通過其他一些特殊要求的結構,它可以模仿許多其他語言中的特性。來試試看吧!
作者:Andrew Gerrand