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