go言語で複数のgoroutineのエラーハンドリングをする
関連
複数のgoroutineの結果の取得
複数のgoroutineの結果の取得1(エラーが起きると中断する例)
第5章 並行プログラミング―ゴルーチンとチャネルを使いこなす:はじめてのGo―シンプルな言語仕様,型システム,並行処理|gihyo.jp … 技術評論社
こちらのコードを参考に以下のような複数のgoroutineを実行してその結果を取得する場合を考える
package main import ( "fmt" "log" "net/http" ) func getStatus(urls []string) <-chan string { statusChan := make(chan string) for _, url := range urls { go func(url string) { res, err := http.Get(url) if err != nil { log.Fatal(err) } statusChan <- res.Status }(url) } return statusChan } func main() { urls := []string{"https://www.google.com", "https://www.yahoo.co.jp/"} statusChan := getStatus(urls) for i := 0; i < len(urls); i++ { fmt.Println(<-statusChan) } }
これを実行すると以下のような結果になる
$go run goroutine0.go 200 OK 200 OK
しかし、このコードの場合、どれか一つでもhttp.Getに失敗するとそこで処理を中断してしまうようになっている
試しに、上のurlsを以下のように書き換えてみる
urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"}
実行するとhttps://badhostの時点で終了する
$go run goroutine0.go 2019/05/12 07:14:01 Get https://badhost: dial tcp: lookup badhost: no such host exit status 1
もしhttp.Getに失敗した場合に、そのままstatusChan <- res.Statusをしようとすると、 以下のようなnil pointer参照のpanicが起きてしまうので、このコードではhttp.Getが失敗した時点でlog.Fatalによってエラーメッセージとともに処理を中断することになっている
http.Get error: Get https://badhost: dial tcp: lookup badhost: no such host panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x40 pc=0x5ec38e]
エラーが起きても処理を中断してほしくない場合は以下のように書ける
複数のgoroutineの結果の取得2(エラー処理ができない例)
上のコードを元に、エラーが起きてもその他の処理を進めるようにしたのが以下になる
package main import ( "fmt" "net/http" "time" ) func main() { checkStatus := func(urls []string) <-chan *http.Response { resultChan := make(chan *http.Response) for _, url := range urls { go func(url string) { resp, err := http.Get(url) // http.Getに時間がかかった場合を模するためにsleep fmt.Println("sleep 2 sec") time.Sleep(2 * time.Second) if err != nil { fmt.Printf("http.Get error: %v\n", err) } resultChan <- resp }(url) } return resultChan } urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} resultChan := checkStatus(urls) for i := 0; i < len(urls); i++ { result := <-resultChan if result == nil { fmt.Printf("Response: nil\n") continue } fmt.Printf("Response: %s\n", result.Status) } }
- http.Getでerrorが生じてもnil pointerのpanicが起きないように、「resultChan <- resp」のように結果をまるごと入れている。Statusは関数の呼び出し側で見ることにする
- http.Getで失敗すると当然respはnilなので、channelの呼び出しの際に、「if result == nil」という条件分岐をしている
これを実行すると以下のようになる
$go run goroutine2.go sleep 2 sec sleep 2 sec sleep 2 sec http.Get error: Get https://badhost: dial tcp: lookup badhost: no such host Response: nil Response: 200 OK Response: 200 OK
これはなかなか良さそうだが、関数の呼び出し側でエラーハンドリングができないという問題がある。
ここでは、nilが返ってきたことしかわからない
できれば「Get https://badhost: dial tcp: lookup badhost: no such host」を関数の呼び出し側で出力できるようにしたい
複数のgoroutineの結果の取得3(呼び出し側でエラー処理ができるようにした例)
Go言語による並行処理 などを読むと以下のようにErrorとResponseをひとまとめにした構造体を返せばいいとある concurrency-in-go-src/fig-patterns-proper-err-handling.go at 4e55fd7f3f5b9c5efc45a841702393a1485ba206 · kat-co/concurrency-in-go-src · GitHub
これを参考に上のコードを以下のように直してみる
package main import ( "fmt" "net/http" "time" ) func main() { type Result struct { Error error Response *http.Response } checkStatus := func(urls []string) <-chan Result { resultChan := make(chan Result) for _, url := range urls { go func(url string) { resp, err := http.Get(url) fmt.Println("sleep 2 sec") time.Sleep(2 * time.Second) resultChan <- Result{Error: err, Response: resp} }(url) } return resultChan } urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} result := checkStatus(urls) for i := 0; i < len(urls); i++ { res := <-result if res.Error != nil { fmt.Printf("error: %v\n", res.Error) continue } fmt.Printf("Response: %v\n", res.Response.Status) } }
res.Errorを関数の呼び出し側で出力できた。 これができればエラーの時は~などの処理がやりやすくなりそう
複数のgoroutineの結果の取得4(呼び出し側でエラー処理ができるようにした例 ※for range使用)
上のコードでも問題ないと思うが、勉強のために別の書き方も書いてみたい
ここまではchannelの読み込みを以下のようにしていたが、
result := checkStatus(urls) for i := 0; i < len(urls); i++ { res := <-result ~ }
channelは以下のような受け取り方もできる
for result := range checkStatus(urls) { ~ }
ただ、for rangeの書き方をするには注意が必要になる
- for rangeでchannelを受け取る場合、goroutine側でchennelのcloseが必要になる
- closeしないと終わりがわからずmain側のfor range文が延々と待ち続けることになる
- では単純にgoroutineの中で以下のように「defer close(results) 」してもいいのかというとそうではない
// うまくいかない例 func main() { type Result struct { Error error Response *http.Response } checkStatus := func(urls []string) <-chan Result { resultChan := make(chan Result, 10) for _, url := range urls { go func(url string) { defer close(resultChan) // ここでcloseすると複数goroutine全部を待てない resp, err := http.Get(url) fmt.Println("sleep 2 sec") time.Sleep(2 * time.Second) resultChan <- Result{Error: err, Response: resp} }(url) } return resultChan } urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} for result := range checkStatus(urls) { if result.Error != nil { fmt.Printf("error: %v\n", result.Error) continue } fmt.Printf("Response: %v\n", result.Response.Status) } }
これを実行すると以下のようになる
sleep 2 sec sleep 2 sec sleep 2 sec error: Get https://badhost: dial tcp: lookup badhost: no such host
goroutineが一つしか実行されていない
複数のgoroutineの処理を待ってからclose(resultChan)をするために、sync.WaitGroupを使ってみた
package main import ( "fmt" "net/http" "sync" "time" ) func main() { type Result struct { Error error Response *http.Response } checkStatus := func(urls []string) <-chan Result { resultChan := make(chan Result, 10) wg := new(sync.WaitGroup) defer close(resultChan) for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() resp, err := http.Get(url) fmt.Println("sleep 2 sec") time.Sleep(2 * time.Second) resultChan <- Result{Error: err, Response: resp} }(url) } wg.Wait() return resultChan } urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} for result := range checkStatus(urls) { if result.Error != nil { fmt.Printf("error: %v\n", result.Error) continue } fmt.Printf("Response: %v\n", result.Response.Status) } }
実行結果は以下のようになる
sleep 2 sec sleep 2 sec sleep 2 sec error: Get https://badhost: dial tcp: lookup badhost: no such host Response: 200 OK Response: 200 OK
やりたいことが実現できた
複数のgoroutineの結果の取得5(呼び出し側でエラー処理ができるようにした例 ※for range使用 goroutineリークを避けるように工夫したもの)
上のコードは、selectを使って外部からgoroutineを中断するように変えると、goroutineリークによってメモリ使用率を圧迫することを避けることができる
package main import ( "fmt" "net/http" "sync" "time" ) func main() { type Result struct { Error error Response *http.Response } checkStatus := func(done <-chan interface{}, urls []string) <-chan Result { resultChan := make(chan Result, 10) wg := new(sync.WaitGroup) defer close(resultChan) for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() resp, err := http.Get(url) fmt.Println("sleep 2 sec") time.Sleep(2 * time.Second) select { case <-done: return case resultChan <- Result{Error: err, Response: resp}: } }(url) } wg.Wait() return resultChan } done := make(chan interface{}) defer close(done) urls := []string{"https://www.google.com", "https://badhost", "https://www.yahoo.co.jp/"} for result := range checkStatus(done, urls) { if result.Error != nil { fmt.Printf("error: %v\n", result.Error) continue } fmt.Printf("Response: %v\n", result.Response.Status) } }
実行結果はこうなる
sleep 2 sec sleep 2 sec sleep 2 sec error: Get https://badhost: dial tcp: lookup badhost: no such host Response: 200 OK Response: 200 OK