見出し画像

【Go初学】Go言語による並行処理-エラーハンドリング

▼概要

Go言語による並行処理」でなるほどなと思った点について。
「4.5エラーハンドリング」で紹介されていた考え方がためになった。

▼エラーハンドリングの根本的な疑問

プログラムを組むうえで「誰がその責務を負うのか」というのがある。例えばバックエンドのAPIを実装する際、リクエスト情報の解析やビジネスロジック処理、データベースの操作、レスポンスデータの加工などを1つのクラスで実装してしまうと保守性が悪く処理の共有も出来なくなる、単純に見通しが良くないなど問題になる。そこでそれぞれの責務やレイヤーに応じて分散してクラスを作成して組み合わせることでそういった問題が解消する。
本では、エラーの扱いにおいても同様で最も根本的な疑問は「誰がそのエラーを処理する責任を持つべきか」であると述べている。

下記は本と類似したサンプルで、main() から呼び出した関数から、チャネルを経由して非同期で値を受け取る形になっている。呼び出された非同期処理内ではエラーが発生する可能性があり、発生した際はその旨を伝える実装をおこなっている。

func main() {
  for res := range someCalc([]int{0, 1, 2, 3, 4, 5}) {
    log.Printf("値 = %d", res)
  }
}

func someCalc(numbers []int) <-chan int {
  result := make(chan int)
  rand.Seed(time.Now().UnixNano())

  go func() {
    defer close(result)

    sort.Ints(numbers)
    errorValue := rand.Intn(numbers[len(numbers)-1])
    log.Printf("errorValue: %d", errorValue)

    for _, x := range numbers {
      time.Sleep(1 * time.Second)

      // エラー発生
      if x == errorValue {
        log.Printf("エラー発生: %d", x)
      }

      result <- x
    }
  }()

  return result
}

ここで問題なのは、呼び出し側と呼び出される側の両方で、エラーが発生した状態を正常に扱うことを配慮し忘れていることにある。

呼び出し側は、呼び出した関数の中でエラーが発生する可能性を考慮し、その際にどのように振る舞えばよいかを実装する必要がある。
呼び出される側は、自身の責務の中で処理を行う際にエラーが発生した際、自身の管理する範囲で必要な対応を行うと同時に、呼び出し側へ通知して、より上流のコンテキスト(状態・状況)の中で適切な対応を行えるようにする必要がある。

▼適切な対応

上記の問題に対して、本では以下の対応が推奨されている。

エラーはGoルーチンから返される値を構築する際の第一級市民として捉えられるべきであるということです。もし今書いているGoルーチンがエラーを生成するのであれば、それらは正常系の結果と強く結びつけて - ちょうど通常の同期関数と同じように - 正常系と同じ経路を使って渡されるべきです。

呼び出し側は呼び出される側よりもプログラム全体に関してより多くのコンテキストを持っており、エラーに対してより賢明な判断が下せる。上記の通り、非同期処理においても同期処理と同じように正常系の結果とエラーを同列で扱うよう修正したものが下記の形になる。

func main() {
  for res := range someCalc([]int{0, 1, 2, 3, 4, 5}) {
    if res.Error != nil {
      // エラー時の適切な処理を行う
      // 例えばログの出力や処理の中断
      log.Printf("%v", res.Error)
      break
    }
    log.Printf("値 = %d", res)
  }
}

func someCalc(numbers []int) <-chan Result {  // Result構造体を返す
  result := make(chan Result)
  rand.Seed(time.Now().UnixNano())

  go func() {
    defer close(result)

    sort.Ints(numbers)
    errorValue := rand.Intn(numbers[len(numbers)-1])
    log.Printf("errorValue: %d", errorValue)

    for _, x := range numbers {
      time.Sleep(1 * time.Second)

      // エラー発生
      if x == errorValue {
        result <- Result{Error: fmt.Errorf("エラー発生: %d", x)}
      }

      result <- Result{Value: x, Error: nil}
    }
  }()

  return result
}

type Result struct {
  Value int
  Error error
}

正常系の値とエラーを構造体でまとめて処理の結果として返すことで、同期関数の時と同じようにエラーのハンドリングが行えるようになった。

あとがき

自身の責務においてエラーが発生するか意識し、捕捉すること。そして別のプログラムから呼ばれる場合は、より適切な判断が出来る上流へエラーを結果として返すこと。基本的なためGoに限らず役立てる考え方だと思う。
今回は単純な例だが、複数の非同期処理を扱う場合などでは処理を中断する必要があるか、どのように中断するか、プログラムの状態をどのように更新するか、様々な状況によって変わると思われるため1つ1つ丁寧にハンドリングできるようにすることが大事だと思った。

この記事が気に入ったらサポートをしてみませんか?