[翻譯]Go的Defer、Panic和Recover

dont-panic.png

簡(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ī)則:

  1. 函數(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
}
  1. 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)
    }
}
  1. 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • golang中defer,panic,recover是很常用的三個(gè)特性,三者一起使用可以充當(dāng)其他語(yǔ)言中try…ca...
    smoke_zl閱讀 27,958評(píng)論 2 28
  • 1、簡(jiǎn)介 Go具有控制流程的常用機(jī)制:if,for,switch,goto。 它還有g(shù)o語(yǔ)句在單獨(dú)的gorouti...
    沈淵閱讀 1,100評(píng)論 0 2
  • 今天同樣,給大家分享下施羅特中簡(jiǎn)易的三維矯正運(yùn)動(dòng),這種運(yùn)動(dòng)適合一些弧度介于15°至25°的輕微脊柱側(cè)患者。一...
    南柯一夢(mèng)wf閱讀 702評(píng)論 0 0
  • 世界那么美 哪有空去傷悲 打破吧 所有的枷鎖 放下吧 所有的束縛 這世界也沒那么美 哪有空去傷悲
    我心飛翔lijing閱讀 224評(píng)論 0 1
  • vue去掉#號(hào)需要在 router上加上以下參數(shù): mode: 'history',base:'/項(xiàng)目名稱/目錄...
    張大丶閱讀 1,629評(píng)論 0 0

友情鏈接更多精彩內(nèi)容