# 4.2 錯誤值檢查
我們先來看第一個問題:如何對一個傳播鏈條中的錯誤類型進行斷言?
在標準庫中,`errors`包中最為重要的一個`New`函數能夠從給定格式的字符串中創建一個錯誤, 它的內部實現僅僅是對`error`接口的一個實現`errorString`:
```
package errors
type errorString struct { s string }
func (e *errorString) Error() string { return e.s }
func New(text string) error { return &errorString{text} }
```
當然,這遠遠不夠。為了能夠對錯誤進行格式化,在使用 Go 的過程中通常還會需要將`New`與`fmt.Sprintf`進行組合,達到格式化的目的:
```
func E(format string, a ...interface{}) error {
return errors.New(fmt.Sprintf(format, a...))
}
```
但這種依靠字符串進行錯誤定義的方式的可處理性幾乎為零,將會在調用上下文之間引入強依賴, 因為一個具體的錯誤值在`fmt`格式化封裝的過程中被轉移為了一個字符串類型,進而不能對 錯誤傳播過程中錯誤的來源進行斷言。 為此,Go 在`errors`包中引入了一系列 API 來增強錯誤檢查的手段。
## 4.2.1 錯誤傳播鏈
首先,為了建立錯誤傳播鏈,`fmt.Errorf`函數允許使用`%w`動詞對一個錯誤進行包裝。 在`Errorf`的實現中,它會將需要包裝的`err`包裝為一個實現了`Error() string`和`Unwrap() error`兩個接口的`wrapError`結構,其包含需要封裝的新錯誤消息以及原始錯誤:
```
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string { return e.msg }
func (e *wrapError) Unwrap() error { return e.err }
```
`fmt`包本身對格式化的支持定義了`pp`結構,會將格式化后的內容存儲在`buf`中。 但在錯誤傳播鏈條的包裝上,為了不破壞原始錯誤值,額外使用了`wrapErrs`和`wrappedErr`兩個字段,其中`wrapErrs`用于格式化過程中判斷是否對錯誤進行了包裝,`wrappedErr`則用于存儲原始的錯誤:
```
type pp struct {
buf buffer // 本質為 []byte 類型
...
wrapErrs bool
wrappedErr error // wrappedErr 記錄了 %w 動詞的 err
}
```
方法`Errorf`會首先使用`newPrinter`和`doPrintf`對格式進行處理, 將帶有動詞的格式字符串和參數進行拼接。 具體而言,`Errorf`總是假設出現`%w`動詞,并`doPrintf`函數內部將對`error`類型的參數進行特殊處理。當有錯誤保存在`wrappedErr`時,說明需要對 錯誤進行一層包裝,否則說明是一個原始的錯誤構造:
```
package fmt
import "errors"
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true // 假設格式化過程中可能包含 %w 動詞,設置為 true
p.doPrintf(format, a) // 對 format 和實際的參數進行拼接,用于后續打印
s := string(p.buf) // 拼接好的內容保存在 buf 內
var err error
if p.wrappedErr == nil {
err = errors.New(s) // 構造原始錯誤
} else {
err = &wrapError{s, p.wrappedErr} // 對錯誤進行包裝
}
p.free()
return err
}
```
`doPrintf`函數最終將調用`handleMethods`方法來對錯誤進行記錄。當遇到`%w`動詞時,會判斷`%w`對應的參數值是否為`error`類型,并將錯誤保存到`wrappedErr`內,并將后續處理退化為`%v`的后續拼接與格式化。
```
// 調用鏈 doPrintf -> printArg -> handleMethods
func (p *pp) handleMethods(verb rune) (handled bool) {
...
if verb == 'w' {
err, ok := p.arg.(error)
// 判斷與 %w 對應的值是否為 error 類型,否則處理為錯誤的動詞組合
if !ok || !p.wrapErrs || p.wrappedErr != nil {
...
return true
}
// 保存 err,并將其退化為 %v 動詞
p.wrappedErr = err
verb = 'v'
}
...
}
```
顯然,`%w`這個動詞的主要目的是將`err`記錄到`wrappedErr`這個同時實現了`Error() string`和`Unwrap() error`的錯誤中, 從而能安全的將`verb`轉化為`%v`動詞對參數進行后續的格式化拼接。
## 4.2.2 錯誤值拆包
但形成錯誤鏈條后,使用`Unwrap`便能將一個已被`fmt`包裝過的`error`進行拆包, 其實現的核心思想是對錯誤值是否實現了`Unwrap() error`方法進行一次類型斷言:
```
func Unwrap(err error) error {
// 斷言 err 實現了 Unwrap 方法
u, ok := err.(interface { Unwrap() error })
if !ok { return nil }
return u.Unwrap()
}
```
在`fmt.Errorf`的實現中,已經看到,錯誤鏈條錯誤使用了`wrapError`進行包裝, 而這一類型恰好實現了`Unwrap() error`方法。
## 4.2.3 錯誤斷言
`Is`用于檢查當前的兩個錯誤是否相等。之所以需要這個函數是因為一個錯誤可能被包裝了多層, 那么我們需要支持這個錯誤在包裝過多層后的判斷。 可想而知,在實現上需要一個`for`循環對其進行`Unwrap`操作:
```
func Is(err, target error) bool {
if target == nil { return err == target }
isComparable := reflect.TypeOf(target).Comparable()
for {
// 如果 target 錯誤是可比較的,則直接進行比較
if isComparable && err == target { return true }
// 如果 err 實現了 Is 方法,則調用其實現進行判斷
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 否則,對 err 進行 Unwrap
if err = Unwrap(err); err == nil { return false }
// 如果 Unwrap 成功,則繼續判斷
}
}
```
可見`Is`方法的目的是替換使用`==`形式的錯誤斷言:
```
if err == io.ErrUnexpectedEOF {
// ... 處理錯誤
}
=>
if errors.Is(err, io.ErrUnexpectedEOF) {
// ... 處理錯誤
}
```
值得注意的是,`Is`方法要求自定義的錯誤值實現`Is(error) bool`方法來進行自定義的錯誤斷言, 否則錯誤的比較仍然只是使用`==`算符。
方法`As`的實現與`Is`基本類似,但不同之處在于`As`的目的是將某個錯誤給拆封 到具體的變量中,因此對于一個錯誤鏈而言,需要一個循環不斷對錯誤進行`Unwrap`, 當錯誤值實現了`As(interface{}) bool`方法時,則可完成拆封:
```
func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflect.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflect.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
if e := typ.Elem(); e.Kind() != reflect.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
// 若可直接將 err 拆封到 target
if reflect.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflect.ValueOf(err))
return true
}
// 判斷 err 是否實現 As 方法,若已實現則直接調用
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// 否則對錯誤鏈進行 Unwrap
err = Unwrap(err)
}
return false
}
var errorType = reflect.TypeOf((*error)(nil)).Elem()
```
可見,由于錯誤鏈的存在,`errors.As`方法的目的是替換類型斷言式的錯誤斷言:
```
if e, ok := err.(*os.PathError); ok {
// ... 處理錯誤
}
=>
var e *os.PathError
if errors.As(err, &e) {
// ... 處理錯誤
}
```
## 4.2.4 小結
`errors`包中對錯誤檢查的設計通過暴露`New`、`Unwrap`、`Is`和`As`四個方法完成 在復雜函數調用鏈條中使用`fmt.Errorf`封裝的錯誤傳播鏈條的拆解。 其中`New`負責原始錯誤的創建,`Unwrap`允許對錯誤傳播鏈條進行一次拆包,`Is`則提供了在復雜錯誤鏈中,對錯誤類型進行斷言的能力; 而`As`解決了將錯誤從錯誤鏈拆解到某個目標錯誤類型的能力。
- 第一部分 :基礎篇
- 第1章 Go語言的前世今生
- 1.2 Go語言綜述
- 1.3 順序進程通訊
- 1.4 Plan9匯編語言
- 第2章 程序生命周期
- 2.1 從go命令談起
- 2.2 Go程序編譯流程
- 2.3 Go 程序啟動引導
- 2.4 主Goroutine的生與死
- 第3 章 語言核心
- 3.1 數組.切片與字符串
- 3.2 散列表
- 3.3 函數調用
- 3.4 延遲語句
- 3.5 恐慌與恢復內建函數
- 3.6 通信原語
- 3.7 接口
- 3.8 運行時類型系統
- 3.9 類型別名
- 3.10 進一步閱讀的參考文獻
- 第4章 錯誤
- 4.1 問題的演化
- 4.2 錯誤值檢查
- 4.3 錯誤格式與上下文
- 4.4 錯誤語義
- 4.5 錯誤處理的未來
- 4.6 進一步閱讀的參考文獻
- 第5章 同步模式
- 5.1 共享內存式同步模式
- 5.2 互斥鎖
- 5.3 原子操作
- 5.4 條件變量
- 5.5 同步組
- 5.6 緩存池
- 5.7 并發安全散列表
- 5.8 上下文
- 5.9 內存一致模型
- 5.10 進一步閱讀的文獻參考
- 第二部分 運行時篇
- 第6章 并發調度
- 6.1 隨機調度的基本概念
- 6.2 工作竊取式調度
- 6.3 MPG模型與并發調度單
- 6.4 調度循環
- 6.5 線程管理
- 6.6 信號處理機制
- 6.7 執行棧管理
- 6.8 協作與搶占
- 6.9 系統監控
- 6.10 網絡輪詢器
- 6.11 計時器
- 6.12 非均勻訪存下的調度模型
- 6.13 進一步閱讀的參考文獻
- 第7章 內存分配
- 7.1 設計原則
- 7.2 組件
- 7.3 初始化
- 7.4 大對象分配
- 7.5 小對象分配
- 7.6 微對象分配
- 7.7 頁分配器
- 7.8 內存統計
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 寫屏幕技術
- 8.3 調步模型與強弱觸發邊界
- 8.4 掃描標記與標記輔助
- 8.5 免清掃式位圖技術
- 8.6 前進保障與終止檢測
- 8.7 安全點分析
- 8.8 分代假設與代際回收
- 8.9 請求假設與實務制導回收
- 8.10 終結器
- 8.11 過去,現在與未來
- 8.12 垃圾回收統一理論
- 8.13 進一步閱讀的參考文獻
- 第三部分 工具鏈篇
- 第9章 代碼分析
- 9.1 死鎖檢測
- 9.2 競爭檢測
- 9.3 性能追蹤
- 9.4 代碼測試
- 9.5 基準測試
- 9.6 運行時統計量
- 9.7 語言服務協議
- 第10章 依賴管理
- 10.1 依賴管理的難點
- 10.2 語義化版本管理
- 10.3 最小版本選擇算法
- 10.4 Vgo 與dep之爭
- 第12章 泛型
- 12.1 泛型設計的演進
- 12.2 基于合約的泛型
- 12.3 類型檢查技術
- 12.4 泛型的未來
- 12.5 進一步閱讀的的參考文獻
- 第13章 編譯技術
- 13.1 詞法與文法
- 13.2 中間表示
- 13.3 優化器
- 13.4 指針檢查器
- 13.5 逃逸分析
- 13.6 自舉
- 13.7 鏈接器
- 13.8 匯編器
- 13.9 調用規約
- 13.10 cgo與系統調用
- 結束語: Go去向何方?