Go 併發 2.2:錯誤處理模式

banq發表於2024-04-23

使用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)
  }
 }()
}

解釋

  1. 我們使用在 Go Concurrency 1.3 — Sync Package | Channels & Select 中學習到的模式建立了一個 done 通道,用於向子 go 例程指示程式的終止。此外,在呼叫 close() 之前,我們在該通道上新增了 6 秒鐘的睡眠時間,以指示子 go 例程的終止。
  2. 我們定義了 seedNumbers() 以向介面提供的通道播種數字。請注意我們是如何將兩個值初始化的,如果呼叫 modTwo() 會導致錯誤。我們利用在 Go Concurrency 2.1 - Patterns and Idioms | Fundamentals中學習的模式來處理通道。
  3. 在函式 modTwo() 中,我們有兩個 if 塊檢查錯誤並將錯誤列印到 stdout。
  4. 如果收到一個有效的可整除值,我們將繼續檢查並列印該值是否能被 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
  }
 }()
}

解釋:

  1. 我們建立了兩個通道,一個用於透過 seedNumbers() 為輸入值播種,另一個用於讀寫 modTwo() 的輸出。此外,我們推遲 close(),以便 range 語句知道通道已關閉,不再等待進一步讀取或寫入。
  2. 我們在 modTwo() 函式中定義了可能導致錯誤的介面片段值。
  3. 我們對 rawStream 進行測距,並將這些值寫入 inputStream,以允許 modTwo() 讀取並驗證其中的值。我們新增了等待時間,以觀察程式在 goroutines 停止時的暫停情況。
  4. 我們將 outputStream 包裝成一種型別,以幫助我們將所需的值轉發給負責處理輸出的程式,或提供處理輸出的資訊,即 main() 程式。
  5. 在 outputStream 的範圍內,main() 程式將決定如何處理響應並列印相應的資訊。

結論
在使用 goroutines 時,將潛在結果與潛在錯誤聯絡起來,有助於我們將錯誤處理與工作 goroutines 區分開來。這反過來又使我們的程式具有可組合性,並使程式設計師能夠輕鬆除錯潛在的問題。
 

相關文章