Go 語(yǔ)言 Recover捕獲異常

2023-03-14 16:54 更新

原文鏈接:https://gopl-zh.github.io/ch5/ch5-10.html


5.10. Recover捕獲異常

通常來(lái)說(shuō),不應(yīng)該對(duì)panic異常做任何處理,但有時(shí),也許我們可以從異常中恢復(fù),至少我們可以在程序崩潰前,做一些操作。舉個(gè)例子,當(dāng)web服務(wù)器遇到不可預(yù)料的嚴(yán)重問(wèn)題時(shí),在崩潰前應(yīng)該將所有的連接關(guān)閉;如果不做任何處理,會(huì)使得客戶端一直處于等待狀態(tài)。如果web服務(wù)器還在開(kāi)發(fā)階段,服務(wù)器甚至可以將異常信息反饋到客戶端,幫助調(diào)試。

如果在deferred函數(shù)中調(diào)用了內(nèi)置函數(shù)recover,并且定義該defer語(yǔ)句的函數(shù)發(fā)生了panic異常,recover會(huì)使程序從panic中恢復(fù),并返回panic value。導(dǎo)致panic異常的函數(shù)不會(huì)繼續(xù)運(yùn)行,但能正常返回。在未發(fā)生panic時(shí)調(diào)用recover,recover會(huì)返回nil。

讓我們以語(yǔ)言解析器為例,說(shuō)明recover的使用場(chǎng)景。考慮到語(yǔ)言解析器的復(fù)雜性,即使某個(gè)語(yǔ)言解析器目前工作正常,也無(wú)法肯定它沒(méi)有漏洞。因此,當(dāng)某個(gè)異常出現(xiàn)時(shí),我們不會(huì)選擇讓解析器崩潰,而是會(huì)將panic異常當(dāng)作普通的解析錯(cuò)誤,并附加額外信息提醒用戶報(bào)告此錯(cuò)誤。

func Parse(input string) (s *Syntax, err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p)
        }
    }()
    // ...parser...
}

deferred函數(shù)幫助Parse從panic中恢復(fù)。在deferred函數(shù)內(nèi)部,panic value被附加到錯(cuò)誤信息中;并用err變量接收錯(cuò)誤信息,返回給調(diào)用者。我們也可以通過(guò)調(diào)用runtime.Stack往錯(cuò)誤信息中添加完整的堆棧調(diào)用信息。

不加區(qū)分的恢復(fù)所有的panic異常,不是可取的做法;因?yàn)樵趐anic之后,無(wú)法保證包級(jí)變量的狀態(tài)仍然和我們預(yù)期一致。比如,對(duì)數(shù)據(jù)結(jié)構(gòu)的一次重要更新沒(méi)有被完整完成、文件或者網(wǎng)絡(luò)連接沒(méi)有被關(guān)閉、獲得的鎖沒(méi)有被釋放。此外,如果寫(xiě)日志時(shí)產(chǎn)生的panic被不加區(qū)分的恢復(fù),可能會(huì)導(dǎo)致漏洞被忽略。

雖然把對(duì)panic的處理都集中在一個(gè)包下,有助于簡(jiǎn)化對(duì)復(fù)雜和不可以預(yù)料問(wèn)題的處理,但作為被廣泛遵守的規(guī)范,你不應(yīng)該試圖去恢復(fù)其他包引起的panic。公有的API應(yīng)該將函數(shù)的運(yùn)行失敗作為error返回,而不是panic。同樣的,你也不應(yīng)該恢復(fù)一個(gè)由他人開(kāi)發(fā)的函數(shù)引起的panic,比如說(shuō)調(diào)用者傳入的回調(diào)函數(shù),因?yàn)槟銦o(wú)法確保這樣做是安全的。

有時(shí)我們很難完全遵循規(guī)范,舉個(gè)例子,net/http包中提供了一個(gè)web服務(wù)器,將收到的請(qǐng)求分發(fā)給用戶提供的處理函數(shù)。很顯然,我們不能因?yàn)槟硞€(gè)處理函數(shù)引發(fā)的panic異常,殺掉整個(gè)進(jìn)程;web服務(wù)器遇到處理函數(shù)導(dǎo)致的panic時(shí)會(huì)調(diào)用recover,輸出堆棧信息,繼續(xù)運(yùn)行。這樣的做法在實(shí)踐中很便捷,但也會(huì)引起資源泄漏,或是因?yàn)閞ecover操作,導(dǎo)致其他問(wèn)題。

基于以上原因,安全的做法是有選擇性的recover。換句話說(shuō),只恢復(fù)應(yīng)該被恢復(fù)的panic異常,此外,這些異常所占的比例應(yīng)該盡可能的低。為了標(biāo)識(shí)某個(gè)panic是否應(yīng)該被恢復(fù),我們可以將panic value設(shè)置成特殊類(lèi)型。在recover時(shí)對(duì)panic value進(jìn)行檢查,如果發(fā)現(xiàn)panic value是特殊類(lèi)型,就將這個(gè)panic作為error處理,如果不是,則按照正常的panic進(jìn)行處理(在下面的例子中,我們會(huì)看到這種方式)。

下面的例子是title函數(shù)的變形,如果HTML頁(yè)面包含多個(gè)<title>,該函數(shù)會(huì)給調(diào)用者返回一個(gè)錯(cuò)誤(error)。在soleTitle內(nèi)部處理時(shí),如果檢測(cè)到有多個(gè)<title>,會(huì)調(diào)用panic,阻止函數(shù)繼續(xù)遞歸,并將特殊類(lèi)型bailout作為panic的參數(shù)。

// soleTitle returns the text of the first non-empty title element
// in doc, and an error if there was not exactly one.
func soleTitle(doc *html.Node) (title string, err error) {
    type bailout struct{}
    defer func() {
        switch p := recover(); p {
        case nil:       // no panic
        case bailout{}: // "expected" panic
            err = fmt.Errorf("multiple title elements")
        default:
            panic(p) // unexpected panic; carry on panicking
        }
    }()
    // Bail out of recursion if we find more than one nonempty title.
    forEachNode(doc, func(n *html.Node) {
        if n.Type == html.ElementNode && n.Data == "title" &&
            n.FirstChild != nil {
            if title != "" {
                panic(bailout{}) // multiple titleelements
            }
            title = n.FirstChild.Data
        }
    }, nil)
    if title == "" {
        return "", fmt.Errorf("no title element")
    }
    return title, nil
}

在上例中,deferred函數(shù)調(diào)用recover,并檢查panic value。當(dāng)panic value是bailout{}類(lèi)型時(shí),deferred函數(shù)生成一個(gè)error返回給調(diào)用者。當(dāng)panic value是其他non-nil值時(shí),表示發(fā)生了未知的panic異常,deferred函數(shù)將調(diào)用panic函數(shù)并將當(dāng)前的panic value作為參數(shù)傳入;此時(shí),等同于recover沒(méi)有做任何操作。(請(qǐng)注意:在例子中,對(duì)可預(yù)期的錯(cuò)誤采用了panic,這違反了之前的建議,我們?cè)诖酥皇窍胂蜃x者演示這種機(jī)制。)

有些情況下,我們無(wú)法恢復(fù)。某些致命錯(cuò)誤會(huì)導(dǎo)致Go在運(yùn)行時(shí)終止程序,如內(nèi)存不足。

練習(xí)5.19: 使用panic和recover編寫(xiě)一個(gè)不包含return語(yǔ)句但能返回一個(gè)非零值的函數(shù)。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)