ErrGroup: Goで並行処理を制する鍵

[![Group58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/353fd88c-8b8d-4bf1-afe6-9a9d81025dde.png)](https://leapcell.io/?lc_t=q_errgroup)



**[Leapcell: ウェブホスティング、非同期タスク、Redisのための次世代サーバレスプラットフォーム](https://leapcell.io/?lc_t=q_errgroup)**

# Go言語のerrgroupライブラリ：強力な並列処理制御ツール

`errgroup`は、公式のGoライブラリ`x`内のユーティリティで、複数の`goroutines`を並列実行し、エラーを処理するために使用されます。これは、`sync.WaitGroup`をベースに`errgroup.Group`を実装しており、並列プログラミングにより強力な機能を提供します。

## errgroupの利点
`sync.WaitGroup`と比較して、`errgroup.Group`には以下の利点があります：
1. **エラー処理**：`sync.WaitGroup`は`goroutines`の完了を待つだけで、戻り値やエラーを処理しません。一方、`errgroup.Group`は直接戻り値を処理することはできませんが、`goroutine`がエラーに遭遇した場合、直ちに他の実行中の`goroutines`をキャンセルし、`Wait`メソッドで最初の`nil`でないエラーを返すことができます。
2. **コンテキストキャンセル**：`errgroup`は`context.Context`と組み合わせて使用できます。`goroutine`がエラーに遭遇した場合、自動的に他の`goroutines`をキャンセルし、効果的にリソースを制御し、不要な作業を回避します。
3. **並列プログラミングの簡素化**：`errgroup`を使用することで、エラー処理のための定型コードを削減できます。開発者は手動でエラー状態や同期ロジックを管理する必要がなく、並列プログラミングをより簡単かつ保守しやすくできます。
4. **並列度の制限**：`errgroup`は、並列実行する`goroutines`の数を制限するインターフェイスを提供し、過負荷を回避することができます。これは`sync.WaitGroup`にはない機能です。

## sync.WaitGroupの使用例
`errgroup.Group`を紹介する前に、まず`sync.WaitGroup`の使い方を見てみましょう。
```go
package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {
    var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
        "http://www.somestupidname.com/", 
    }
    var err error

    var wg sync.WaitGroup 

    for _, url := range urls {
        wg.Add(1) 

        go func() {
            defer wg.Done() 

            resp, e := http.Get(url)
            if e != nil { 
                err = e
                return
            }
            defer resp.Body.Close()
            fmt.Printf("fetch url %s status %s\n", url, resp.Status)
        }()
    }

    wg.Wait()
    if err != nil { 
        fmt.Printf("Error: %s\n", err)
    }
}
```
実行結果：
```
$ go run examples/main.go
fetch url http://www.google.com/ status 200 OK
fetch url http://www.golang.org/ status 200 OK
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
```
`sync.WaitGroup`の典型的なアイディオム：
```go
var wg sync.WaitGroup

for ... {
    wg.Add(1)

    go func() {
        defer wg.Done()
        // do something
    }()
}

wg.Wait()
```

## errgroup.Groupの使用例
### 基本的な使い方
`errgroup.Group`の使用パターンは`sync.WaitGroup`と似ています。
```go
package main

import (
    "fmt"
    "net/http"
    "golang.org/x/sync/errgroup"
)

func main() {
    var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
        "http://www.somestupidname.com/", 
    }

    var g errgroup.Group 

    for _, url := range urls {
        g.Go(func() error {
            resp, err := http.Get(url)
            if err != nil {
                return err 
            }
            defer resp.Body.Close()
            fmt.Printf("fetch url %s status %s\n", url, resp.Status)
            return nil 
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Printf("Error: %s\n", err)
    }
}
```
実行結果：
```
$ go run examples/main.go
fetch url http://www.google.com/ status 200 OK
fetch url http://www.golang.org/ status 200 OK
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
```

### コンテキストキャンセル
`errgroup`は`errgroup.WithContext`を提供して、キャンセル機能を追加します。
```go
package main

import (
    "context"
    "fmt"
    "net/http"
    "sync"
    "golang.org/x/sync/errgroup"
)

func main() {
    var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
        "http://www.somestupidname.com/", 
    }

    g, ctx := errgroup.WithContext(context.Background())

    var result sync.Map

    for _, url := range urls {
        g.Go(func() error {
            req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
            if err != nil {
                return err 
            }

            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return err 
            }
            defer resp.Body.Close()

            result.Store(url, resp.Status)
            return nil 
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Println("Error: ", err)
    }

    result.Range(func(key, value any) bool {
        fmt.Printf("fetch url %s status %s\n", key, value)
        return true
    })
}
```
実行結果：
```
$ go run examples/withcontext/main.go
Error:  Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
fetch url http://www.google.com/ status 200 OK
```
[http://www.somestupidname.com/](http://www.somestupidname.com/) へのリクエストがエラーを報告したため、プログラムは [http://www.golang.org/](http://www.golang.org/) へのリクエストをキャンセルしました。

### 並列度の制限
`errgroup`は`errgroup.SetLimit`を提供して、同時に実行される`goroutines`の数を制限します。
```go
package main

import (
    "fmt"
    "time"
    "golang.org/x/sync/errgroup"
)

func main() {
    var g errgroup.Group
    g.SetLimit(3)

    for i := 1; i <= 10; i++ {
        g.Go(func() error {
            fmt.Printf("Goroutine %d is starting\n", i)
            time.Sleep(2 * time.Second) 
            fmt.Printf("Goroutine %d is done\n", i)
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Printf("Encountered an error: %v\n", err)
    }

    fmt.Println("All goroutines complete.")
}
```
実行結果：
```
$  go run examples/main.go
Goroutine 3 is starting
Goroutine 1 is starting
Goroutine 2 is starting
Goroutine 2 is done
Goroutine 1 is done
Goroutine 5 is starting
Goroutine 3 is done
Goroutine 6 is starting
Goroutine 4 is starting
Goroutine 6 is done
Goroutine 5 is done
Goroutine 8 is starting
Goroutine 4 is done
Goroutine 7 is starting
Goroutine 9 is starting
Goroutine 9 is done
Goroutine 8 is done
Goroutine 10 is starting
Goroutine 7 is done
Goroutine 10 is done
All goroutines complete.
```

### 試して開始
`errgroup`は`errgroup.TryGo`を提供して、タスクを試して開始します。これは`errgroup.SetLimit`と組み合わせて使用する必要があります。
```go
package main

import (
    "fmt"
    "time"
    "golang.org/x/sync/errgroup"
)

func main() {
    var g errgroup.Group
    g.SetLimit(3)

    for i := 1; i <= 10; i++ {
        if g.TryGo(func() error {
            fmt.Printf("Goroutine %d is starting\n", i)
            time.Sleep(2 * time.Second) 
            fmt.Printf("Goroutine %d is done\n", i)
            return nil
        }) {
            fmt.Printf("Goroutine %d started successfully\n", i)
        } else {
            fmt.Printf("Goroutine %d could not start (limit reached)\n", i)
        }
    }

    if err := g.Wait(); err != nil {
        fmt.Printf("Encountered an error: %v\n", err)
    }

    fmt.Println("All goroutines complete.")
}
```
実行結果：
```
$ go run examples/main.go
Goroutine 1 started successfully
Goroutine 1 is starting
Goroutine 2 is starting
Goroutine 2 started successfully
Goroutine 3 started successfully
Goroutine 4 could not start (limit reached)
Goroutine 5 could not start (limit reached)
Goroutine 6 could not start (limit reached)
Goroutine 7 could not start (limit reached)
Goroutine 8 could not start (limit reached)
Goroutine 9 could not start (limit reached)
Goroutine 10 could not start (limit reached)
Goroutine 3 is starting
Goroutine 2 is done
Goroutine 3 is done
Goroutine 1 is done
All goroutines complete.
```

## ソースコード解説
`errgroup`のソースコードは主に3つのファイルで構成されています：
- [メインロジックコード](https://github.com/golang/sync/blob/v0.8.0/errgroup/errgroup.go)
- [Go 1.20以降のバージョン向けのwithCancelCauseの実装](https://github.com/golang/sync/blob/v0.8.0/errgroup/go120.go)
- [Go 1.20未満のバージョン向けのwithCancelCauseの実装](https://github.com/golang/sync/blob/v0.8.0/errgroup/pre_go120.go)

### コア構造
```go
type token struct{}

type Group struct {
    cancel func(error)
    wg sync.WaitGroup
    sem chan token
    errOnce sync.Once
    err     error
}
```
- `token`：信号を渡すための空の構造体で、並列度を制御するために使用されます。
- `Group`：
  - `cancel`：コンテキストがキャンセルされたときに呼び出される関数。
  - `wg`：内部で使用される`sync.WaitGroup`。
  - `sem`：並列コルーチンの数を制御する信号チャネル。
  - `errOnce`：エラーが1回だけ処理されることを保証します。
  - `err`：最初のエラーを記録します。

### 主なメソッド
- **SetLimit**：並列度を制限します。
```go
func (g *Group) SetLimit(n int) {
    if n < 0 {
        g.sem = nil
        return
    }
    if len(g.sem) != 0 {
        panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem)))
    }
    g.sem = make(chan token, n)
}
```
- **Go**：新しいコルーチンを開始してタスクを実行します。
```go
func (g *Group) Go(f func() error) {
    if g.sem != nil {
        g.sem <- token{}
    }

    g.wg.Add(1)
    go func() {
        defer g.done()

        if err := f(); err != nil {
            g.errOnce.Do(func() {
                g.err = err
                if g.cancel != nil {
                    g.cancel(g.err)
                }
            })
        }
    }()
}
```
- **Wait**：すべてのタスクが完了するのを待ち、最初のエラーを返します。
```go
func (g *Group) Wait() error {
    g.wg.Wait()
    if g.cancel != nil {
        g.cancel(g.err)
    }
    return g.err
}
```
- **TryGo**：タスクを試して開始します。
```go
func (g *Group) TryGo(f func() error) bool {
    if g.sem != nil {
        select {
        case g.sem <- token{}:
        default:
            return false
        }
    }

    g.wg.Add(1)
    go func() {
        defer g.done()

        if err := f(); err != nil {
            g.errOnce.Do(func() {
                g.err = err
                if g.cancel != nil {
                    g.cancel(g.err)
                }
            })
        }
    }()
    return true
}
```

## まとめ
`errgroup`は、公式の拡張ライブラリであり、`sync.WaitGroup`のベースにエラー処理機能を追加しており、同期、エラー伝播、コンテキストキャンセルなどの機能を提供します。その`WithContext`メソッドはキャンセル機能を追加でき、`SetLimit`は並列度を制限でき、`TryGo`はタスクを試して開始できます。ソースコードは巧みに設計されており、参考に値するものです。 


# [Leapcell: ウェブホスティング、非同期タスク、Redisのための次世代サーバレスプラットフォーム](https://leapcell.io/?lc_t=q_errgroup)

最後に、Go言語をデプロイするのに最適なプラットフォームをおすすめします：**[Leapcell](https://leapcell.io/?lc_t=q_errgroup)**

[![barndpic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/aae494e9-229e-4f0b-9477-b101140d5254.png)](https://leapcell.io/?lc_t=q_errgroup)


### 1. 多言語対応  
- JavaScript、Python、Go、またはRustで開発できます。  

### 2. 無料で無制限のプロジェクトをデプロイ
- 使用量に応じてのみ支払い — リクエストがなければ、請求もありません。

### 3. 抜群のコスト効率
- 実行した分だけ請求され、アイドル時には請求されません。
- 例：平均応答時間60ミリ秒で694万件のリクエストに対応可能で、費用は25ドル。

### 4. 合理化された開発者体験
- 直感的なUIにより、簡単にセットアップできます。
- 完全自動化されたCI/CDパイプラインとGitOpsの統合。
- 実行時のメトリクスとログ記録により、アクション可能なインサイトを提供します。

### 5. 簡単なスケーラビリティと高パフォーマンス
- 高い並列度を容易に処理できる自動スケーリング機能。
- 運用オーバーヘッドはゼロ — 構築に集中できます。

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/9567433c-9c89-4070-a6ae-e37dfc0fa4c7.png)](https://leapcell.io/?lc_t=q_errgroup)


[ドキュメントでさらに詳しく探る！](https://docs.leapcell.io/)

LeapcellのTwitter：[https://x.com/LeapcellHQ](https://x.com/LeapcellHQ)