## 5.4. 錯誤
在Go中有一部分函數總是能成功的運行。比如strings.Contains和strconv.FormatBool函數,對各種可能的輸入都做了良好的處理,使得運行時幾乎不會失敗,除非遇到災難性的、不可預料的情況,比如運行時的內存溢出。導致這種錯誤的原因很復雜,難以處理,從錯誤中恢復的可能性也很低。
還有一部分函數只要輸入的參數滿足一定條件,也能保證運行成功。比如time.Date函數,該函數將年月日等參數構造成time.Time對象,除非最后一個參數(時區)是nil。這種情況下會引發panic異常。panic是來自被調用函數的信號,表示發生了某個已知的bug。一個良好的程序永遠不應該發生panic異常。
對于大部分函數而言,永遠無法確保能否成功運行。這是因為錯誤的原因超出了程序員的控制。舉個例子,任何進行I/O操作的函數都會面臨出現錯誤的可能,只有沒有經驗的程序員才會相信讀寫操作不會失敗,即使是簡單的讀寫。因此,當本該可信的操作出乎意料的失敗后,我們必須弄清楚導致失敗的原因。
在Go的錯誤處理中,錯誤是軟件包API和應用程序用戶界面的一個重要組成部分,程序運行失敗僅被認為是幾個預期的結果之一。
對于那些將運行失敗看作是預期結果的函數,它們會返回一個額外的返回值,通常是最后一個,來傳遞錯誤信息。如果導致失敗的原因只有一個,額外的返回值可以是一個布爾值,通常被命名為ok。比如,cache.Lookup失敗的唯一原因是key不存在,那么代碼可以按照下面的方式組織:
```Go
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist…
}
```
通常,導致失敗的原因不止一種,尤其是對I/O操作而言,用戶需要了解更多的錯誤信息。因此,額外的返回值不再是簡單的布爾類型,而是error類型。
內置的error是接口類型。我們將在第七章了解接口類型的含義,以及它對錯誤處理的影響。現在我們只需要明白error類型可能是nil或者non-nil。nil意味著函數運行成功,non-nil表示失敗。對于non-nil的error類型,我們可以通過調用error的Error函數或者輸出函數獲得字符串類型的錯誤信息。
```Go
fmt.Println(err)
fmt.Printf("%v", err)
```
通常,當函數返回non-nil的error時,其他的返回值是未定義的(undefined),這些未定義的返回值應該被忽略。然而,有少部分函數在發生錯誤時,仍然會返回一些有用的返回值。比如,當讀取文件發生錯誤時,Read函數會返回可以讀取的字節數以及錯誤信息。對于這種情況,正確的處理方式應該是先處理這些不完整的數據,再處理錯誤。因此對函數的返回值要有清晰的說明,以便于其他人使用。
在Go中,函數運行失敗時會返回錯誤信息,這些錯誤信息被認為是一種預期的值而非異常(exception),這使得Go有別于那些將函數運行失敗看作是異常的語言。雖然Go有各種異常機制,但這些機制僅被使用在處理那些未被預料到的錯誤,即bug,而不是那些在健壯程序中應該被避免的程序錯誤。對于Go的異常機制我們將在5.9介紹。
Go這樣設計的原因是由于對于某個應該在控制流程中處理的錯誤而言,將這個錯誤以異常的形式拋出會混亂對錯誤的描述,這通常會導致一些糟糕的后果。當某個程序錯誤被當作異常處理后,這個錯誤會將堆棧跟蹤信息返回給終端用戶,這些信息復雜且無用,無法幫助定位錯誤。
正因此,Go使用控制流機制(如if和return)處理錯誤,這使得編碼人員能更多的關注錯誤處理。
{% include "./ch5-04-1.md" %}
{% include "./ch5-04-2.md" %}
- 前言
- 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:其它語言