Recipe

Go errgroup primer

The errgroup package from golang.org/x/sync is the canonical way to coordinate goroutines that can fail. It batches concurrent work, propagates the first error, and cancels in-flight siblings through a shared context.

1. Why errgroup beats sync.WaitGroup

A raw sync.WaitGroup waits but cannot surface errors and has no built-in cancellation. With errgroup you get both: the first non-nil error returned by any goroutine short-circuits the group, cancels the derived context, and bubbles up through Wait().

2. Fan-out with shared context

Use errgroup.WithContext(parent) to derive a context that is cancelled when any goroutine returns an error. Pass that context into every blocking call so HTTP requests, DB queries, and timers terminate cleanly instead of leaking.

3. Reference implementation

Parallel HTTP fetch with bounded fan-out and first-error cancellation:

package main

import (
	"context"
	"fmt"
	"net/http"

	"golang.org/x/sync/errgroup"
)

func fetchAll(ctx context.Context, urls []string) error {
	g, ctx := errgroup.WithContext(ctx)
	results := make([]int, len(urls))

	for i, url := range urls {
		i, url := i, url
		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()
			results[i] = resp.StatusCode
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		return fmt.Errorf("fetch failed: %w", err)
	}
	return nil
}