# 4.5 錯誤處理的未來
TODO: 討論社區里的一些優秀的方案、以及未來可能的設計
## 4.5.1 來自社區的方案
在錯誤處理這件事情上,其實社區提供了許多非常優秀的方案, 其中一個非常出色的工作來自 Dave Cheney 和他的錯誤原語。
### 錯誤原語
`pkg/errors`與標準庫中`errors`包不同,它首先提供了`Wrap`:
```
func Wrap(err error, message string) error {
if err == nil {
return nil
}
// 首先將錯誤產生的上下文進行保存
err = &withMessage{
cause: err,
msg: message,
}
// 再將 withMessage 錯誤的調用堆棧保存為 withStack 錯誤
return &withStack{
err,
callers(),
}
}
type withMessage struct {
cause error
msg string
}
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
func (w *withMessage) Cause() error { return w.cause }
type withStack struct {
error
*stack // 攜帶 stack 的信息
}
func (w *withStack) Cause() error { return w.error }
func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}
```
這是一種依賴運行時接口的解決方案,通過`runtime.Caller`來獲取錯誤出現時的堆棧信息。通過`Wrap()`產生的錯誤類型`withMessage`還實現了`causer`接口:
```
type causer interface {
Cause() error
}
```
當我們需要對一個錯誤進行檢查時,則可以通過`errors.Cause(err error)`來返回一個錯誤產生的原因,進而獲得了錯誤產生的上下文信息:
```
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok { break }
err = cause.Cause()
}
return err
}
```
進而可以做到:
```
switch err := errors.Cause(err).(type) {
case *CustomError:
// ...
}
```
得益于`fmt.Formatter`接口,`pkg/errors`還實現了`Fomat(fmt.State, rune)`方法, 進而在使用`%+v`進行錯誤打印時,能攜帶堆棧信息:
```
func (w *withStack) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') { // %+v 支持攜帶堆棧信息的輸出
fmt.Fprintf(s, "%+v", w.Cause())
w.stack.Format(s, verb) // 將 runtime.Caller 獲得的信息進行打印
return
}
fallthrough
case 's':
io.WriteString(s, w.Error())
case 'q':
fmt.Fprintf(s, "%q", w.Error())
}
}
```
得到形如下面格式的錯誤輸出:
```
current message: causer message
main.causer
/path/to/caller/main.go:5
main.caller
/path/to/caller/main.go:12
main.main
/path/to/caller/main.go:27
```
### 基于錯誤鏈的高層抽象
我們再來看另一種錯誤處理的哲學,現在我們來考慮下面這個例子:
```
conn, err := net.Dial("tcp", "localhost:1234")
if err != nil {
panic(err)
}
_, err := conn.Write(command1)
if err != nil {
panic(err)
}
r := bufio.NewReader(conn)
status, err := r.ReadString('\n')
if err != nil {
panic(err)
}
if status == "ok" {
_, err := conn.Write(command2)
if err != nil {
panic(err)
}
}
```
我們很明確的能夠觀察到錯誤處理帶來的問題:為清晰的閱讀代碼的整體邏輯帶來了障礙。我們希望上面的代碼能夠清晰的展現最重要的代碼邏輯:
```
conn := net.Dial("tcp", "localhost:1234")
conn.Write(command1)
r := bufio.NewReader(conn)
status := r.ReadString('\n')
if status == "ok" {
conn.Write(command2)
}
```
如果我們進一步觀察這個問題的現象,可以將整段代碼抽象為圖 1 所示的邏輯結構。
**圖 1: 產生分支的錯誤處理手段**
如果我們嘗試將這段充滿分支的邏輯進行高層抽象,將其轉化為一個單一鏈條,則能夠得到 圖 2 所示的隱式錯誤鏈條。
**圖 2: 消除分支的鏈式錯誤處理手段**
則能夠得到下面的代碼:
```
type SafeConn struct {
conn net.Conn
r *bufio.Reader
status string
err error
}
func safeDial(n, addr string) SafeConn {
conn, err := net.Dial(n, addr)
r := bufio.NewReader(conn)
return SafeConn{conn, r, "ok", err}
}
func (c *SafeConn) write(b []byte) {
if c.err != nil && status == "ok" { return }
_, c.err = c.conn.Write(b)
}
func (c *SafeConn) read() {
if err != nil { return }
c.status, c.err = c.r.ReadString('\n')
}
```
則當建立連接時候:
```
c := safeDial("tcp", "localhost:1234") // 如果此條指令出錯
c.write(command1) // 不會發生任何事情
c.read() // 不會發生任何事情
c.write(command2) // 不會發生任何事情
// 最后對進行整個流程的錯誤處理
if c.err != nil || c.status != "ok" {
panic("bad connection")
}
```
這種將錯誤進行高層抽象的方法通常包含以下四個一般性的步驟:
1. 建立一種新的類型
2. 將原始值進行封裝
3. 將原始行為進行封裝
4. 將分支條件進行封裝
## 4.5.2 其他可能的設計
TODO:
Generics + Error handling?
Either Coproduct
[https://www.ituring.com.cn/article/508191](https://www.ituring.com.cn/article/508191)[https://www.bookstack.cn/read/mostly-adequate-guide-chinese/ch8.4](https://www.bookstack.cn/read/mostly-adequate-guide-chinese/ch8.4)
## 4.5.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去向何方?