使用Go併發幫助我們解決使用 goroutine 時的錯誤處理問題。
錯誤處理
錯誤處理需要與同步程式設計不同的模式。為了更好地理解這個問題,讓我們看一個簡單的程式:
package main
import ( "fmt" "time" )
func main() { // 1. done := make(chan interface{}) inputStream := make(chan interface{}) go func() { time.Sleep(time.Second * 6) close(done) }() go seedNumbers(done, inputStream) go modTwo(done, inputStream) <-done } func seedNumbers(done <-chan interface{}, inputStream chan<- interface{}){ // 2. stream := []interface{}{"abc", 1, "2", 3, 4} go func() { for v := range stream{ select { case <-done: return default: inputStream <- v } } }() } func modTwo(done, inputStream <-chan interface{}){ go func() { for { select { case <-done: return case v := <-inputStream: intV, ok := v.(int) // 3. if !ok { fmt.Printf("\n seeded value not of type int: %+v", v) continue } if intV == 0 { fmt.Println("seeded value is zero: cannot mod with zero") continue } // 4. if intV % 2 == 0{ fmt.Printf("\n %d is divisible by two", v) }else{ fmt.Printf("\n %d is not divisible by two", v) } } time.Sleep(time.Second) } }() }
|
解釋
- 我們使用在 Go Concurrency 1.3 — Sync Package | Channels & Select 中學習到的模式建立了一個 done 通道,用於向子 go 例程指示程式的終止。此外,在呼叫 close() 之前,我們在該通道上新增了 6 秒鐘的睡眠時間,以指示子 go 例程的終止。
- 我們定義了 seedNumbers() 以向介面提供的通道播種數字。請注意我們是如何將兩個值初始化的,如果呼叫 modTwo() 會導致錯誤。我們利用在 Go Concurrency 2.1 - Patterns and Idioms | Fundamentals中學習的模式來處理通道。
- 在函式 modTwo() 中,我們有兩個 if 塊檢查錯誤並將錯誤列印到 stdout。
- 如果收到一個有效的可整除值,我們將繼續檢查並列印該值是否能被 2 整除。
夠簡單嗎?在牢記這一計劃的同時,也要問自己一些問題。
- 如果我想捕獲無效輸入流並分別處理它們,會發生什麼情況?
- 主程式如何判斷是否應該終止或處理錯誤?
- 如何擺脫 done 和 close(done) 方法,並保證迭代次數與輸入次數相等?
讓我們看看下面的重構程式。
package main
import ( "fmt" "time" ) type Result struct { Input interface{} DivisibleByTwo bool Error error } func main() { // 1. outputStream := make(chan Result) inputStream := make(chan interface{}) defer close(inputStream) defer close(outputStream) // 2. stream := []interface{}{"abc", 1, 0, 3, 4} go seedNumbers(stream, inputStream) go modTwo(inputStream, outputStream) for i:=0; i < len(stream); i++ { r := <- outputStream // 5. if r.Error != nil{ fmt.Printf("\n input : %+v, err - %s", r.Input, r.Error) }else{ fmt.Printf("\n %d is divisible by two: %t", r.Input.(int), r.DivisibleByTwo) } } } func seedNumbers(rawStream []interface{}, inputStream chan<- interface{}) { go func() { for _, v := range rawStream { // 3. inputStream <- v time.Sleep(time.Second) } }() } func modTwo(inputStream <-chan interface{}, outputStream chan<- Result) { go func() { for v := range inputStream { // 4. r := Result{Input: v} intV, ok := v.(int) if !ok { r.Error = fmt.Errorf("seeded value not of type int: %+v", v) outputStream <- r continue } if intV == 0 { r.Error = fmt.Errorf("seeded value is zero: cannot mod with zero") outputStream <- r continue } if intV%2 == 0 { r.DivisibleByTwo = true } outputStream <- r } }() }
|
解釋:
- 我們建立了兩個通道,一個用於透過 seedNumbers() 為輸入值播種,另一個用於讀寫 modTwo() 的輸出。此外,我們推遲 close(),以便 range 語句知道通道已關閉,不再等待進一步讀取或寫入。
- 我們在 modTwo() 函式中定義了可能導致錯誤的介面片段值。
- 我們對 rawStream 進行測距,並將這些值寫入 inputStream,以允許 modTwo() 讀取並驗證其中的值。我們新增了等待時間,以觀察程式在 goroutines 停止時的暫停情況。
- 我們將 outputStream 包裝成一種型別,以幫助我們將所需的值轉發給負責處理輸出的程式,或提供處理輸出的資訊,即 main() 程式。
- 在 outputStream 的範圍內,main() 程式將決定如何處理響應並列印相應的資訊。
結論
在使用 goroutines 時,將潛在結果與潛在錯誤聯絡起來,有助於我們將錯誤處理與工作 goroutines 區分開來。這反過來又使我們的程式具有可組合性,並使程式設計師能夠輕鬆除錯潛在的問題。