簡(jiǎn)書不維護(hù)了,歡迎關(guān)注我的知乎:波羅學(xué)的個(gè)人主頁(yè)
翻譯自:https://blog.golang.org/defer-panic-and-recover
Go有和其他語言一樣常見的流程控制語句:if, for, switch, goto靶端。同時(shí)也有g(shù)o表達(dá)式來實(shí)現(xiàn)在不同的goroutine中運(yùn)行代碼(并發(fā))球凰。而今天我們將討論的是go的異常控制流程:defer神僵、panic和recover酌心。
Defer
defer語句會(huì)將函數(shù)推入到一個(gè)列表中。同時(shí)列表中的函數(shù)會(huì)在return語句執(zhí)行后被調(diào)用挑豌。defer常常會(huì)被用來簡(jiǎn)化資源清理釋放之類的操作安券。
舉個(gè)例子,我們來觀察下下面這個(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)閉源文件的情況下立即返回址貌。此問題可以很容易地通過在第二個(gè)return語句前調(diào)用src.Close
來補(bǔ)救。但如果函數(shù)的功能特別復(fù)雜,該問題就可能不是那么容易被發(fā)現(xiàn)和解決了练对。下面介紹一下defer遍蟋,通過它,我們將可以確保文件總是能被正常關(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語句讓我們?cè)诖蜷_文件時(shí)便要思考文件的關(guān)閉虚青,不必在意過多return語句,便可實(shí)現(xiàn)資源的正確釋放螺男。
Defer語句的行為是明確可知的棒厘,此處有三條簡(jiǎn)單的規(guī)則:
- 函數(shù)參數(shù)值由defer語句調(diào)用時(shí)確定
比如下面這個(gè)例子,打印出來的變量i的值即是運(yùn)行到defer語句時(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語句之后按照先進(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(類似其他語言的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可以通過直接調(diào)用panic
產(chǎn)生。同時(shí)也可能由運(yùn)行時(shí)的錯(cuò)誤所產(chǎn)生敞掘,例如數(shù)組越界訪問叽掘。
Recover
recover是go語言的內(nèi)置函數(shù),它的主要作用是可以從panic的重新奪回goroutine的控制權(quán)玖雁。Recover必須通過defer來運(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通過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就不會(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]
下面我們來看一個(gè)真實(shí)的案例,來自go標(biāo)準(zhǔn)庫(kù)的json包拳喻。它先通過一系列的遞歸函數(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來返回錯(cuò)誤信息。
下面是defer其他的一些使用場(chǎng)景(除了前面列出的file.close案例)捌归,例如鎖的釋放:
mu.Lock()
defer mu.Unlock()
打印頁(yè)尾:
printHeader()
defer printFooter()
and more.
總的來說肛响,defer為我們提供了一種異常強(qiáng)大的流程控制機(jī)制(不僅僅限于panic、recover場(chǎng)景)惜索。而且通過其他一些特殊要求的結(jié)構(gòu)特笋,它可以模仿許多其他語言中的特性。來試試看吧巾兆!
作者:Andrew Gerrand