kikitori Tech Blog

株式会社kikitoriは、農業流通現場のDXを実現するSaaS『nimaru』と青果店『KAJITSU』を運営する会社です。

Goのerrgroupパッケージ

はじめに

こんにちは。kikitoriの坂村です。 私が担当しているプラットフォーム事業についてはバックエンドはGoを使って開発しているのですが、先日Claude Codeと実装を進めていると初めて見るerrgroupなるパッケージに触れる機会があったのでメモがてらの共有&解説記事となります。

errgroup なにやつ?

golang.org/x/sync/errgroup 実は2016年ころに準標準パッケージとしてリリースされてたようでした。10年前からあったのにまったく触れる機会なく車輪の再発明のような実装を繰り返してきて恥ずかしい限りですが、AIで開発することによって逆に新たなインプットを得られることもあるんだなと見逃してやってください。。。

何をやってくれるかというと、たとえば複数のマイクロサービスに対してデータをかき集めてゴニョゴニョするんだけど、もし何かしらのマイクロサービスでエラーになったら他の並列処理はキャンセルして、呼び出し元に処理を戻すときなんかを想像してもらうと良いかと思います。(地道に実装すると結構めんどいやつ)

使い方サンプル

   g, gctx := errgroup.WithContext(ctx)
    for i := 0; i < 10; i++ {
        g.Go(func() error {
            path, err := DoSomething(gctx)
            if err != nil {
                return err
            }
        })
    }
    if err := g.Wait(); err != nil {
        return err
    }
  1. errgroup.WithContextで複数のgoroutineを管理するGroupを作成(1行目)
  2. それを使ってg.Goで非同期実行(3行目)
  3. g.Waitで非同期で走らせてるgoroutineの終了を待つ(10行目)

こんだけ。めっちゃ簡単。

ソースを読んでみる(v0.16.0)

type Group struct {
    cancel func(error)

    wg sync.WaitGroup

    sem chan token

    errOnce sync.Once
    err     error
}


func WithContext(ctx context.Context) (*Group, context.Context) {
    ctx, cancel := context.WithCancelCause(ctx)
    return &Group{cancel: cancel}, ctx
}

最初のWithContextの中身的にはWithCancelCauseで引数のctxをwrapしてて、管理下のgoroutineの実行を止めたいときに呼ぶためのcancel関数をGroupにプロパティに持っておく形になってます。(contextのキャンセルについてはこれはこれで結構奥深いのでまた別の機会に)

func (g *Group) Go(f func() error) {
    if g.sem != nil {
        g.sem <- token{}
    }

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

        // It is tempting to propagate panics from f()
        // up to the goroutine that calls Wait, but
        // it creates more problems than it solves:
        // - it delays panics arbitrarily,
        //   making bugs harder to detect;
        // - it turns f's panic stack into a mere value,
        //   hiding it from crash-monitoring tools;
        // - it risks deadlocks that hide the panic entirely,
        //   if f's panic leaves the program in a state
        //   that prevents the Wait call from being reached.
        // See #53757, #74275, #74304, #74306.

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

func (g *Group) Wait() error {
    g.wg.Wait()
    if g.cancel != nil {
        g.cancel(g.err)
    }
    return g.err
}

g.semについては同時実行数を制限する際のプロパティのようですが、今回は同時実行数を制限していないので割愛。 やってることとしては、WaitGroupを使って実行数を管理する形でこれはgoroutineを実行する際のテンプレ的な使い方ですね。 g.errOnce.Doのところが複数起動中のgoroutineのうちどれかがエラーになったら他を止めるっていう肝で、一度エラーが発生したら、WithCancelCauseで冒頭に作っておいたcancel関数を実行し、紐づくcontextを全部止めに行ってるわけですね。 ほとんどがgoroutineの管理処理について書かれてますが、この「エラーがあったら他を全停止させる」っていうのがパッケージがerrgroupという名前の所以なのかもですね。

まとめ

今回は割とGopherさん向けの記事になってしまいましたが、こういう気づきを得たら定期的にブログに公開しようと思います。 最後までお付き合いいただきありがとうございました。