## 5.10. Recover捕獲異常
通常來說,不應該對panic異常做任何處理,但有時,也許我們可以從異常中恢復,至少我們可以在程序崩潰前,做一些操作。舉個例子,當web服務器遇到不可預料的嚴重問題時,在崩潰前應該將所有的連接關閉;如果不做任何處理,會使得客戶端一直處于等待狀態。如果web服務器還在開發階段,服務器甚至可以將異常信息反饋到客戶端,幫助調試。
如果在deferred函數中調用了內置函數recover,并且定義該defer語句的函數發生了panic異常,recover會使程序從panic中恢復,并返回panic value。導致panic異常的函數不會繼續運行,但能正常返回。在未發生panic時調用recover,recover會返回nil。
讓我們以語言解析器為例,說明recover的使用場景。考慮到語言解析器的復雜性,即使某個語言解析器目前工作正常,也無法肯定它沒有漏洞。因此,當某個異常出現時,我們不會選擇讓解析器崩潰,而是會將panic異常當作普通的解析錯誤,并附加額外信息提醒用戶報告此錯誤。
```Go
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的參數。
```Go
// 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語句但能返回一個非零值的函數。
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言