# 4.4 錯誤語義
我們其實已經在前面的討論中詳細討論過了錯誤值檢查與錯誤上下文的增強手段, 處理方式啰嗦而冗長,減少這種代碼出現的密集程度真的是一個實際的問題嗎?換句話說: 社區里怨聲載道的冗長的錯誤處理語義,真的有必要進行改進嗎?
### 4.4.1 check/handle 關鍵字
Go 團隊在重新考慮錯誤處理的時候提出過兩種不同的方案, 由 Russ Cox 提出的第一種方案就是引入新的關鍵字`check`/`handle` 進行組合。
我們來看這樣一個復制文件的例子。復制文件操作涉及到源文件的打開、目標文件的創建、 內容的復制、源文件和目標文件的關閉。這之間任何一個環節出錯,都需要錯誤進行處理:
```
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
```
在使用`check`/`handle`組合后,我們可以將前面的代碼進行化簡,較少`if err != nil`的出現頻率,并統一在`handle` 代碼塊中對錯誤進行處理:
```
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close() // 此處發生 err 調用上方的 handle 塊時還會再額外調用一次 w.Close()
return nil
}
```
這種使用`check`和`handle`的方式會當`err`發生時,直接進入`check`關鍵字上方 最近的一個`handle err`塊進行錯誤處理。在官方的這個例子中其實就已經發生了語言上模棱兩可的地方, 當函數最下方的`w.Close`產生調用時, 上方與其最近的一個`handle err`還會再一次調用`w.Close`,這其實是多余的。
此外,這種方式看似對代碼進行了簡化,但仔細一看這種方式與`defer`函數進行錯誤處理之間, 除了減少了`if err != nil { return err }`出現的頻率,并沒有帶來任何本質區別。 例如,我們完全可以使用`defer`來實現`handle`的功能:
```
func CopyFile(src, dst string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
r, err := os.Open(src)
if err != nil { return }
defer r.Close()
w, err := os.Create(dst)
if err != nil { return }
defer func() {
if err != nil {
w.Close()
os.Remove(dst)
}
}()
_, err = io.Copy(w, r)
if err != nil { return }
err = w.Close()
if err != nil { return }
}
```
在仔細衡量后不難看出,`check`/`handle`關鍵字的設計中,`handle`僅僅只是對現有的語義的一個化簡。 具體來說,`handle`關鍵字等價于`defer`:
```
handle err { ... }
=>
defer func() {
if err != nil {
err = ...
}
}()
```
而`check`關鍵字則等價于:
```
check F()
=>
err = F()
if err != nil {
return
}
```
那么能不能僅實現一個`check`關鍵字呢?
### 4.4.2 內建函數`try()`
緊隨`check/handle`的提案,Robert Griesemer 提出了使用內建函數`try()`配合延遲語句來替代`check`,它能夠接收最后一個返回值為`error`的函數, 并將除`error`之外的返回值進行返回,即:
```
x1, x2, ..., xn = try(F())
=>
t1, ..., tn, te := F()
if te != nil {
err = te
return
}
x1, ..., xn = t1, ..., tn
```
有了`try()`函數后,可以將復制文件例子中的代碼化簡為:
```
func CopyFile(src, dst string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // 僅當 try 失敗時才調用
}
}()
try(io.Copy(w, r))
try(w.Close())
return nil
}
```
可見,這種做法與`check/handle`的關鍵字組合本質上也沒有代碼更多思想上的變化, 尤其是`try()`內建函數僅僅在在形式上對`if err != nil { ... }`起到了化簡的作用。
但這一錯誤處理語義并沒有在最后被納入語言規范。 這一設計被拒絕的核心原因是`try()`函數將使對錯誤的調試變得不夠透明, 其本質在于將一個顯式返回的錯誤值進行隱藏。例如,在調試過程中由于被調試函數被包裹在`try()`內,這種不包含錯誤分支的代碼形式,對追蹤錯誤本身是一個毀滅性的打擊,為此用戶不得不在調試時 引入錯誤分支,在調試結束后將錯誤分支消除,煩瑣不堪。
我們從這前后兩份提案中,可以看到 Go 團隊將錯誤處理語義上的改進與 『如何減少`if err != nil { ... }`的出現』直接化了等號,這種純粹寫法風格上的問題, 與 Go 語言早期設計中顯式錯誤值的設計相比,就顯得相形見絀了。
- 第一部分 :基礎篇
- 第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去向何方?