在并發編程中,錯誤處理可能難以正確運行。有時候,我們花了很多時間思考我們的各種流程將如何共享信息和協調,卻忘記考慮如何優雅地處理錯誤。Go避開了流行的錯誤異常模型,Go認為錯誤處理非常重要,并且在開發程序時,我們應該像關注算法一樣關注它。本著這種精神,讓我們來看看在處理多個并發進程時我們如何做到這一點。
思考錯誤處理時最根本的問題是,“應該由誰負責處理錯誤?”在某些情況下,程序需要停止傳遞堆棧中的錯誤,并將它們處理掉,這樣的操作應該何時執行呢?
在并發進程中,這樣的問題變得愈發復雜。因為一個并發進程獨立于其父進程或兄弟進程運行,所以可能很難推斷出錯誤是如何產生的。
下面的就展示了這樣的問題:
```
checkStatus := func(done <-chan interface{}, urls ...string, ) <-chan *http.Response {
responses := make(chan *http.Response)
go func() {
defer close(responses)
for _, url := range urls {
resp, err := http.Get(url)
if err != nil {
fmt.Println(err) //1
continue
}
select {
case <-done:
return
case responses <- resp:
}
}
}()
return responses
}
done := make(chan interface{})
defer close(done)
urls := []string{"https://www.baidu.com", "https://badhost"}
for response := range checkStatus(done, urls...) {
fmt.Printf("Response: %v\n", response.Status)
}
```
1. 這個我們看到goroutine盡其最大努力展示錯誤信號。但也僅僅是展示出來,它還能做什么? 它無法傳回! 如果錯誤種類太多怎么辦? 再請求一遍嗎?
這會輸出:
```
Response: 200 OK
Get https://badhost: dial tcp: lookup badhost on 127.0.1.1:53: no such host
```
我們看到代碼中并沒有給goroutine更多的選擇以處理可能出現的錯誤。它不能簡單的把這個錯誤不加任何處理的拋棄掉,所以當前唯一明智的做法是:它會打印錯誤并希望受到程序使用者的關注。別把你的goroutine像這樣放到如此尷尬的處境之下。我建議你把程序的關注點分離:一般來說,你的并發進程應該把錯誤發送到你的程序的另一部分,這樣程序狀態的完整信息就被保留下來,并留出余地讓使用者可以做出更明智的決定來處理它。我們對上面的例子做了一點點修改:
```
type Result struct { //1
Error error
Response *http.Response
}
checkStatus := func(done <-chan interface{}, urls ...string) <-chan Result { //2
results := make(chan Result)
go func() {
defer close(results)
for _, url := range urls {
var result Result
resp, err := http.Get(url)
result = Result{Error: err, Response: resp} //3
select {
case <-done:
return
case results <- result: //4
}
}
}()
return results
}
done := make(chan interface{})
defer close(done)
urls := []string{"https://www.baidu.com", "https://badhost"}
for result := range checkStatus(done, urls...) {
if result.Error != nil { //5
fmt.Printf("error: %v", result.Error)
continue
}
fmt.Printf("Response: %v\n", result.Response.Status)
}
```
1. 這里我們創建一個包含*http.Response和goroutine循環迭代中可能出現的錯誤類型。
2. 該行返回一個可讀取的通道,以檢索循環迭代的結果。
3. 在這里,我們創建一個Result實例,并設置Error和Response字段。
4. 這是我們將結果寫入通道。
5. 在這里,在我們的main goroutine中,我們能夠自行處理由checkStatus中出現的錯誤,并獲取詳細的響應信息。
這會輸出:
```
Response: 200 OK
error: Get https://badhost: dial tcp: lookup badhost on 127.0.1.1:53: no such host
```
這里要注意的關鍵是我們如何將潛在的結果與潛在的錯誤結合起來。我們已經成功地將錯誤處理的擔憂從生產者中分離出來。這是可取的,因為生成goroutine的goroutine(在這種情況下是我們的main goroutine)擁有更多關于正在運行的程序的上下文,并且可以做出關于如何處理錯誤的更明智的決定。
在前面的例子中,我們只是將錯誤寫入stdio,但我們可以做其他事情。 讓我們稍微修改我們的程序,以便在發生三個或更多錯誤時停止錯誤檢查:
```
done := make(chan interface{})
defer close(done)
errCount := 0
urls := []string{"a", "https://www.baidu.com", "b", "c", "d"}
for result := range checkStatus(done, urls...) {
if result.Error != nil {
fmt.Printf("error: %v\n", result.Error)
errCount++
if errCount >= 3 {
fmt.Println("Too many errors, breaking!")
break
}
continue
}
fmt.Printf("Response: %v\n", result.Response.Status)
}
```
這會輸出:
```
error: Get a: unsupported protocol scheme ""
Response: 200 OK
error: Get b: unsupported protocol scheme ""
error: Get c: unsupported protocol scheme ""
Too many errors, breaking!
```
你可以看到,因為錯誤是從checkStatus返回的而不是在goroutine內部處理的,所以錯誤處理遵循熟悉的Go規范。 這是個簡單的例子,但不難想象在更大更復雜的的程序下是什么樣子。這里的主要內容是,在構建從goroutines返回的價值時,應將錯誤視為一等公民。 如果你的goroutine可能產生錯誤,那么這些錯誤應該與你的結果類型緊密結合,并且通過相同的通信線路傳遞——就像常規的同步函數一樣。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
- 前序
- 誰適合讀這本書
- 章節導讀
- 在線資源
- 第一章 并發編程介紹
- 摩爾定律,可伸縮網絡和我們所處的困境
- 為什么并發編程如此困難
- 數據競爭
- 原子性
- 內存訪問同步
- 死鎖,活鎖和鎖的饑餓問題
- 死鎖
- 活鎖
- 饑餓
- 并發安全性
- 優雅的面對復雜性
- 第二章 代碼建模:序列化交互處理
- 并發與并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并發哲學
- 第三章 Go的并發構建模塊
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select語句
- GOMAXPROCS
- 結論
- 第四章 Go的并發編程范式
- 訪問范圍約束
- fo-select循環
- 防止Goroutine泄漏
- or-channel
- 錯誤處理
- 管道
- 構建管道的最佳實踐
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 隊列
- context包
- 小結
- 第五章 可伸縮并發設計
- 錯誤傳遞
- 超時和取消
- 心跳
- 請求并發復制處理
- 速率限制
- Goroutines異常行為修復
- 本章小結
- 第六章 Goroutines和Go運行時
- 任務調度