こんにちは。KOUIKIです。
Go言語を使ったWeb開発業務に従事しています。
goroutineを使った開発業務が増えてきたので、色々と勉強中です^_^
goroutineではエラーハンドリングが特に難しいと感じます。並行して処理が走るので、どこにエラーの責任を持たせるか、というのが難しい。。。
しかし、以下の書籍で、その問題を解決するヒントを得られたので、記事にしたいと思います。
<目次>
子ゴルーチンでエラーを裁く
goroutineのように並行して処理が走るプログラムを書くときは、どこにエラーを処理させる責任を持たせるかが重要だったりします。
例えば、以下のコードをみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package main import ( "fmt" "net/http" ) func main() { var ( urls = []string{"http://www.googl.com", "https://hoge"} done = make(chan interface{}) ) defer close(done) Do := 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) continue } // gorouineをさばく select { case <-done: return case responses <- resp: } } }() return responses } for response := range Do(done, urls...) { fmt.Printf("Response: %v\n", response.Status) } } |
1 2 3 |
$ go run main.go Response: 200 OK Get "https://hoge": dial tcp: lookup hoge: no such host |
このコードでは、urlsに設定されたURLに対して、リクエストを送っています。
そして、指定されたURLが「存在しない場合」は、エラーが発生します。
このエラー処理の責任を子ゴルーチンに持たせています(main関数が実行されるとメインゴルーチンが作成され、子ゴルーチンを起動する)。
これは結構やってしまいがちなプログラミングパターンですが、あまり良い実装ではありません。
<理由>
1. 子ゴルーチンはメインゴルーチンとは別のプロセスで動いているので、エラー結果を表示させるくらいしかできない
2. 関心事(リクエスト処理とエラーハンドリング)がごっちゃになっている
以下の書籍では、このような場合の正しい解決策を提示しています。
一般的に並行プロセスはエラーを、プログラムの状態を完全に把握したいて何をすべきかをより多くの情報に基づいて決定できる別の箇所へと送るべきです。
Go言語による並行処理
解決方法
という訳で、関心毎を分けてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package main import ( "fmt" "net/http" ) func main() { // レスポンス結果を格納する型を定義 type Result struct { Response *http.Response Error error } var ( urls = []string{"http://www.googl.com", "https://hoge"} done = make(chan interface{}) ) defer close(done) // Resultを返却する Do := func(done <-chan interface{}, urls ...string) <-chan Result { // ResultのChannelを作成する results := make(chan Result) go func() { defer close(results) for _, url := range urls { resp, err := http.Get(url) // エラーハンドリングの代わりに状態を格納 result := Result{Error: err, Response: resp} select { case <-done: return case results <- result: } } }() return results } for result := range Do(done, urls...) { // メインゴルーチンでエラーハンドリングをする if result.Error != nil { fmt.Printf("error: %v\n", result.Error) continue } fmt.Printf("Response: %v\n", result.Response.Status) } } |
1 2 3 |
$ go run main.go Response: 200 OK error: Get "https://hoge": dial tcp: lookup hoge: no such host |
今度は、エラーハンドリングをメインゴルーチンでさばきました。これにより、メインゴルーチン内で、子ゴルーチンから発生したエラーを適切に処理することができるようになりました。
ここで重要なのは、メインゴルーチンでエラーハンドリングをするべきということではありません。
「取得されるであろう結果とエラーを対にする」ということが重要です。こうすることで、エラーが発生した時に処理の責任をもつゴルーチンが何をするべきなのか結果を受け取ることで決定できます。
つまり、エラーハンドリングの懸念と生産者(子ゴルーチン)を無事に切り分けることができたということです。
こうすることで、エラーハンドリングをより柔軟にすることができます。例えば、以下のようにできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
errCount := 0 for result := range Do(done, urls...) { if result.Error != nil { // エラーをカウントする fmt.Printf("error: %v\n", result.Error) errCount++ if errCount >= 3 { fmt.Println("3つ以上エラーが発生しました!") break } continue } fmt.Printf("Response: %v\n", result.Response.Status) } |
上記では、エラーが3つ以上になったらループ処理を終了します。
かなり柔軟性があることがわかると思います!
おわりに
恥ずかしながら今までは、「どこにエラーハンドリングの責任を持たせるか」ということを考えずに、適当に実装していました。
しかし、「Go言語に夜並行処理」を読んで、新しい考え方を取り入れられた気がします。
結構オススメな書籍なので、よかったらご一読ください!
最近のコメント