The errgroup package provides synchronization, error propagation, and context cancellation for groups of goroutines working on subtasks of a common task. It extends sync.WaitGroup with error handling and goroutine limiting capabilities.
import "golang.org/x/sync/errgroup"errgroup.Group manages a collection of goroutines working on related subtasks. Key features:
func WithContext(ctx context.Context) (*Group, context.Context)Creates a new Group and an associated Context derived from the input context.
Parameters:
ctx: Parent context to derive fromReturns:
*Group: New errgroup.Group instancecontext.Context: Derived context that will be canceled when the first function returns a non-nil error or when Wait returnsBehavior: The derived context is canceled automatically in two scenarios:
Example:
g, ctx := errgroup.WithContext(context.Background())
// Use ctx in goroutines to detect when another goroutine failstype Group struct {
// Has unexported fields
}A Group is a collection of goroutines working on subtasks that are part of the same overall task.
Important constraints:
Zero value behavior:
func (g *Group) Go(f func() error)Calls the given function in a new goroutine.
Parameters:
f: Function to execute in a new goroutine. Must return an error or nil.Behavior:
Error handling:
Example:
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
// Do work here
if err := doSomething(); err != nil {
return err // This error will be returned by Wait
}
return nil
})
if err := g.Wait(); err != nil {
log.Printf("task failed: %v", err)
}func (g *Group) TryGo(f func() error) boolAttempts to call the given function in a new goroutine only if the number of active goroutines is currently below the configured limit.
Parameters:
f: Function to execute in a new goroutine. Must return an error or nil.Returns:
bool: true if the goroutine was started, false if the limit was reachedBehavior:
Use cases:
Example:
g := &errgroup.Group{}
g.SetLimit(10)
for i := 0; i < 100; i++ {
i := i
if !g.TryGo(func() error {
return processItem(i)
}) {
// Limit reached, handle the item differently
log.Printf("Skipping item %d, limit reached", i)
}
}
g.Wait()func (g *Group) Wait() errorBlocks until all function calls from the Go method have returned, then returns the first non-nil error (if any) from them.
Returns:
error: The first non-nil error returned by any goroutine, or nil if all succeededBehavior:
Example:
g, ctx := errgroup.WithContext(context.Background())
// Launch multiple goroutines
for i := 0; i < 10; i++ {
i := i
g.Go(func() error {
return processItem(ctx, i)
})
}
// Wait for all to complete
if err := g.Wait(); err != nil {
log.Fatalf("processing failed: %v", err)
}func (g *Group) SetLimit(n int)Limits the number of active goroutines in this group to at most n.
Parameters:
n: Maximum number of concurrent goroutines
Behavior:
Important: The limit must not be modified while any goroutines in the group are active. This will cause a panic.
Example:
g := &errgroup.Group{}
g.SetLimit(5) // Allow at most 5 concurrent goroutines
for i := 0; i < 100; i++ {
i := i
g.Go(func() error {
return processItem(i)
}) // Will block when 5 goroutines are active
}
g.Wait()package main
import (
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
var g errgroup.Group
urls := []string{
"https://example.com",
"https://example.org",
"https://example.net",
}
for _, url := range urls {
url := url // Capture range variable
g.Go(func() error {
return fetch(url)
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Failed: %v\n", err)
}
}package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"time"
)
func main() {
g, ctx := errgroup.WithContext(context.Background())
// Task 1: Long-running operation
g.Go(func() error {
select {
case <-time.After(10 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err() // Canceled by another goroutine's error
}
})
// Task 2: Fails quickly
g.Go(func() error {
time.Sleep(100 * time.Millisecond)
return fmt.Errorf("task 2 failed")
})
// When task 2 fails, ctx is canceled and task 1 exits early
if err := g.Wait(); err != nil {
fmt.Printf("Error: %v\n", err)
}
}package main
import (
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
g := &errgroup.Group{}
g.SetLimit(3) // Process at most 3 items concurrently
items := make([]int, 100)
for i := range items {
i := i
g.Go(func() error {
return processItem(i)
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Processing failed: %v\n", err)
}
}package main
import (
"context"
"golang.org/x/sync/errgroup"
)
func main() {
g, ctx := errgroup.WithContext(context.Background())
in := make(chan int)
out := make(chan int)
// Producer
g.Go(func() error {
defer close(in)
for i := 0; i < 100; i++ {
select {
case in <- i:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
// Workers
const numWorkers = 5
for i := 0; i < numWorkers; i++ {
g.Go(func() error {
for {
select {
case v, ok := <-in:
if !ok {
return nil
}
result, err := process(v)
if err != nil {
return err
}
select {
case out <- result:
case <-ctx.Done():
return ctx.Err()
}
case <-ctx.Done():
return ctx.Err()
}
}
})
}
// Consumer
g.Go(func() error {
for v := range out {
if err := consume(v); err != nil {
return err
}
}
return nil
})
if err := g.Wait(); err != nil {
log.Printf("Pipeline failed: %v", err)
}
}errgroup.Group captures only the first error. If multiple goroutines fail, only the first error (by timing) is returned by Wait:
g := &errgroup.Group{}
g.Go(func() error {
return fmt.Errorf("error 1")
})
g.Go(func() error {
return fmt.Errorf("error 2")
})
err := g.Wait() // Returns either "error 1" or "error 2", depending on timingerrgroup does not recover panics in goroutines. If a function panics, it will crash the entire program:
g := &errgroup.Group{}
g.Go(func() error {
panic("something went wrong") // This will crash the program
})
g.Wait() // Never reachedIf you need panic recovery, wrap your function:
g.Go(func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
// Your code here
return nil
})Always capture range variables: When using errgroup.Go in a loop, capture the loop variable:
for _, item := range items {
item := item // Capture for goroutine
g.Go(func() error {
return process(item)
})
}Use SetLimit before launching goroutines: Calling SetLimit while goroutines are active will panic.
Don't reuse Groups: Create a new Group for each logical group of tasks.
Pass context to goroutines: When using WithContext, pass the derived context to goroutines so they can detect cancellation:
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
return doWork(ctx) // Pass ctx
})Consider TryGo for deadlock prevention: If goroutines might need to spawn other goroutines and you're using SetLimit, use TryGo to prevent deadlocks.