go言語で複数のgoroutineのエラーハンドリングをする

関連

ludwig125.hatenablog.com

複数の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