## 7.11. 基于類型斷言區別錯誤類型
思考在os包中文件操作返回的錯誤集合。I/O可以因為任何數量的原因失敗,但是有三種經常的錯誤必須進行不同的處理:文件已經存在(對于創建操作),找不到文件(對于讀取操作),和權限拒絕。os包中提供了三個幫助函數來對給定的錯誤值表示的失敗進行分類:
```go
package os
func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool
```
對這些判斷的一個缺乏經驗的實現可能會去檢查錯誤消息是否包含了特定的子字符串,
```go
func IsNotExist(err error) bool {
// NOTE: not robust!
return strings.Contains(err.Error(), "file does not exist")
}
```
但是處理I/O錯誤的邏輯可能一個和另一個平臺非常的不同,所以這種方案并不健壯,并且對相同的失敗可能會報出各種不同的錯誤消息。在測試的過程中,通過檢查錯誤消息的子字符串來保證特定的函數以期望的方式失敗是非常有用的,但對于線上的代碼是不夠的。
一個更可靠的方式是使用一個專門的類型來描述結構化的錯誤。os包中定義了一個PathError類型來描述在文件路徑操作中涉及到的失敗,像Open或者Delete操作;并且定義了一個叫LinkError的變體來描述涉及到兩個文件路徑的操作,像Symlink和Rename。這下面是os.PathError:
```go
package os
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
```
大多數調用方都不知道PathError并且通過調用錯誤本身的Error方法來統一處理所有的錯誤。盡管PathError的Error方法簡單地把這些字段連接起來生成錯誤消息,PathError的結構保護了內部的錯誤組件。調用方需要使用類型斷言來檢測錯誤的具體類型以便將一種失敗和另一種區分開;具體的類型可以比字符串提供更多的細節。
```go
_, err := os.Open("/no/such/file")
fmt.Println(err) // "open /no/such/file: No such file or directory"
fmt.Printf("%#v\n", err)
// Output:
// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}
```
這就是三個幫助函數是怎么工作的。例如下面展示的IsNotExist,它會報出是否一個錯誤和syscall.ENOENT(§7.8)或者和有名的錯誤os.ErrNotExist相等(可以在§5.4.2中找到io.EOF);或者是一個`*PathError`,它內部的錯誤是syscall.ENOENT和os.ErrNotExist其中之一。
```go
import (
"errors"
"syscall"
)
var ErrNotExist = errors.New("file does not exist")
// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
```
下面這里是它的實際使用:
```go
_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"
```
如果錯誤消息結合成一個更大的字符串,當然PathError的結構就不再為人所知,例如通過一個對fmt.Errorf函數的調用。區別錯誤通常必須在失敗操作后,錯誤傳回調用者前進行。
- 前言
- 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:其它語言