[TOC]
# 4.1 問題的演化
錯誤`error`在 Go 中表現為一個內建的接口類型,任何實現了`Error() string`方法的類型都能作為`error`類型進行傳遞,成為錯誤值:
```
type error interface {
Error() string
}
```
作為內建接口類型,編譯器負責在參數傳遞檢查時,對值類型所實現的方法進行檢查。 當類型實現了`Error() string`方法后,才允許其作為 error 進行傳遞:
```
// go/src/cmd/compile/internal/gc/universe.go
func makeErrorInterface() *types.Type {
field := types.NewField()
field.Type = types.Types[TSTRING]
f := functypefield(fakeRecvField(), nil, []*types.Field{field})
// 查找是否實現了 Error
field = types.NewField()
field.Sym = lookup("Error")
field.Type = f
t := types.New(TINTER)
t.SetInterface([]*types.Field{field})
return t
}
```
## 4.1.1 錯誤的歷史形態
早期的 Go 甚至沒有錯誤處理 \[Gerrand, 2010\] \[Cox, 2019b\], 當時的`os.Read`函數進行系統調用可能產生錯誤,而該接口是通過`int64`類型進行錯誤返回的:
```
export func Read(fd int64, b *[]byte) (ret int64, errno int64) {
r, e := syscall.read(fd, &b[0], int64(len(b)));
return r, e
}
```
隨后,Go 團隊將這一`errno`轉換抽象成了一個類型:
```
export type Error struct { s string }
func (e *Error) Print() { ... }
func (e *Error) String() string { ... }
export func Read(fd int64, b *[]byte) (ret int64, err *Error) {
r, e := syscall.read(fd, &b[0], int64(len(b)));
return r, ErrnoToError(e)
}
```
之后才演變為了 Go 1 中被人們熟知的`error`接口類型。
可見之所以從理解上我們可以將 error 認為是一個接口,是因為在編譯器實現中, 是通過查詢某個類型是否實現了`Error`方法來創建 Error 類型的。
## 4.1.2 處理錯誤的基本策略
由于 Go 中的錯誤處理設計得非常簡潔,在其他現代編程語言里都幾乎找不見此類做法。 Go 團隊也曾多次撰寫文章來教導 Go 語言的用戶 \[Gerrand, 2011\] \[Pike, 2015\]。 無論怎樣,非常常見的策略包含哨兵錯誤、自定義錯誤以及隱式錯誤三種。
### 哨兵錯誤
哨兵錯誤的處理方式通過特定值表示成功和不同錯誤,依靠調用方對錯誤進行檢查:
```
if err === ErrSomething { ... }
```
例如,比較著名的`io.EOF = errors.New("EOF")`。
這種錯誤處理的方式引入了上下層代碼的依賴,如果被調用方的錯誤類型發生了變化, 則調用方也需要對代碼進行修改:
```
func readf(path string) error {
err := file.Open(path)
if err != nil {
return fmt.Errorf("cannot open file: %v", err)
}
}
func main() {
err := readf("~/.ssh/id_rsa.pub")
if strings.Contains(err.Error(), "not found") {
...
}
}
```
這類錯誤處理的方式是非常危險的,因為它在調用方和被調用方之間建立了牢不可破的依賴關系。 除此之外,哨兵錯誤還有一個相當致命的危險,那就是這種方式所定義的錯誤并非常量,例如:
```
package io
var EOF = errors.New("EOF")
```
而當我們將此錯誤類型公開給其他包使用后,我們非常難以避免這種事情發生:
```
package main
import "io"
func init() {
io.EOF = nil
}
```
這種事情甚至嚴重到,如果在引入的依賴中,有人惡意將這樣驗證錯誤值進行修改的代碼包含進去, 將導致重大的安全問題:
```
import "cropto/rsa"
func init() {
rsa.ErrVerification = nil
}
```
在碩大的代碼依賴中,我們幾乎無法保證這種惡意代碼不會出現在某個依賴的包中。 為了安全起見,變量錯誤類型可以修改為常量錯誤:
```
-var EOF = errors.New("EOF")
+const EOF = ioError("EOF")
+type ioEorror string
+
+func (e ioError) Error() string { return string(e) }
```
### 自定義錯誤
```
if err, ok := err.(SomeErrorType); ok { ... }
```
這類錯誤處理的方式通過自定義的錯誤類型來表示特定的錯誤,同樣依賴上層代碼對錯誤值進行檢查, 不同的是需要使用類型斷言進行檢查。 例如:
```
type CustomizedError struct {
Line int
Msg string
File string
}
func (e CustomizedError) Error() string {
return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}
```
這種錯誤處理的好處在于,可以將錯誤包裝起來,提供更多的上下文信息, 但錯誤的實現方必須向上層公開實現的錯誤類型,不可避免的同樣需要產生依賴關系。
### 隱式錯誤
```
if err != nil { return err }
```
這種錯誤處理的方式直接返回錯誤的任何細節,直接將錯誤進一步報告給上層。這種情況下, 錯誤在當前調用方這里完全沒有進行任何加工,與沒有進行處理幾乎是等價的, 這會產生的一個致命問題在于:丟失調用的上下文信息,如果某個錯誤連續向上層傳播了多次, 那么上層代碼可能在輸出某個錯誤時,根本無法判斷該錯誤的錯誤信息究竟從哪兒傳播而來。 以上面提到的文件打開的例子為例,錯誤信息可能就只有一個`not found`。
## 4.1.3 處理錯誤的本質
回顧處理錯誤的基本策略我們可以看出,在 Go 語言中錯誤處理這一話題基本上是圍繞以下三個問題進行的:
1. 錯誤值檢查:如何對一個傳播鏈條中的錯誤類型進行斷言?
2. 錯誤格式與上下文:出現錯誤時,沒有足夠的堆棧信息,如何增強錯誤發生時的上下文信息并合理格式化一個錯誤?
3. 錯誤處理語義:每個返回錯誤的函數都要求調用方進行顯式處理,處理方式啰嗦而冗長,如何減少這種代碼出現的密集程度?
我們在后面的小節中對這些問題進行一一討論。
- 第一部分 :基礎篇
- 第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去向何方?