gopl-zh.github.com/ch5/ch5-10.md

5.3 KiB
Raw Blame History

5.10. Recover捕獲異常

通常來説不應該對panic異常做任何處理但有時也許我們可以從異常中恢複至少我們可以在程序崩潰前做一些操作。舉個例子當web服務器遇到不可預料的嚴重問題時在崩潰前應該將所有的連接關閉如果不做任何處理會使得客戶端一直處於等待狀態。如果web服務器還在開發階段服務器甚至可以將異常信息反饋到客戶端幫助調試。

如果在deferred函數中調用了內置函數recover併且定義該defer語句的函數發生了panic異常recover會使程序從panic中恢複併返迴panic value。導致panic異常的函數不會繼續運行但能正常返迴。在未發生panic時調用recoverrecover會返迴nil。

讓我們以語言解析器爲例説明recover的使用場景。考慮到語言解析器的複雜性卽使某個語言解析器目前工作正常也無法肯定它沒有漏洞。因此當某個異常出現時我們不會選擇讓解析器崩潰而是會將panic異常當作普通的解析錯誤併附加額外信息提醒用戶報告此錯誤。

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

deferred函數幫助Parse從panic中恢複。在deferred函數內部panic value被附加到錯誤信息中併用err變量接收錯誤信息返迴給調用者。我們也可以通過調用runtime.Stack往錯誤信息中添加完整的堆棧調用信息。

不加區分的恢複所有的panic異常不是可取的做法因爲在panic之後無法保證包級變量的狀態仍然和我們預期一致。比如對數據結構的一次重要更新沒有被完整完成、文件或者網絡連接沒有被關閉、獲得的鎖沒有被釋放。此外如果寫日誌時産生的panic被不加區分的恢複可能會導致漏洞被忽略。

雖然把對panic的處理都集中在一個包下有助於簡化對複雜和不可以預料問題的處理但作爲被廣泛遵守的規范你不應該試圖去恢複其他包引起的panic。公有的API應該將函數的運行失敗作爲error返迴而不是panic。同樣的你也不應該恢複一個由他人開發的函數引起的panic比如説調用者傳入的迴調函數因爲你無法確保這樣做是安全的。

有時我們很難完全遵循規范舉個例子net/http包中提供了一個web服務器將收到的請求分發給用戶提供的處理函數。很顯然我們不能因爲某個處理函數引發的panic異常殺掉整個進程web服務器遇到處理函數導致的panic時會調用recover輸出堆棧信息繼續運行。這樣的做法在實踐中很便捷但也會引起資源洩漏或是因爲recover操作導致其他問題。

基於以上原因安全的做法是有選擇性的recover。換句話説隻恢複應該被恢複的panic異常此外這些異常所占的比例應該盡可能的低。爲了標識某個panic是否應該被恢複我們可以將panic value設置成特殊類型。在recover時對panic value進行檢査如果發現panic value是特殊類型就將這個panic作爲errror處理如果不是則按照正常的panic進行處理在下面的例子中我們會看到這種方式

下面的例子是title函數的變形如果HTML頁面包含多個<title>該函數會給調用者返迴一個錯誤error。在soleTitle內部處理時如果檢測到有多個<title>會調用panic阻止函數繼續遞歸併將特殊類型bailout作爲panic的參數。

// 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函數調用recover併檢査panic value。當panic value是bailout{}類型時deferred函數生成一個error返迴給調用者。當panic value是其他non-nil值時表示發生了未知的panic異常deferred函數將調用panic函數併將當前的panic value作爲參數傳入此時等同於recover沒有做任何操作。請註意在例子中對可預期的錯誤采用了panic這違反了之前的建議我們在此隻是想向讀者演示這種機製。

有些情況下我們無法恢複。某些致命錯誤會導致Go在運行時終止程序如內存不足。

練習5.19 使用panic和recover編寫一個不包含return語句但能返迴一個非零值的函數。