跳至内容

Go Concurrency Patterns in Practice

2026-05-15

I’ve been writing Go for a few years now, and concurrency remains one of those topics where textbook knowledge doesn’t quite prepare you for production. Here are a few patterns I’ve found genuinely useful.

The Worker Pool

The classic. Useful when you need bounded parallelism:

func workerPool(jobs <-chan Job, results chan<- Result, count int) {
    var wg sync.WaitGroup
    for i := 0; i < count; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                results <- process(job)
            }
        }()
    }
    wg.Wait()
    close(results)
}

Context Propagation

Probably the single most important practice I’ve adopted: every long-running goroutine should accept a context. It makes cancellation and timeout handling explicit rather than an afterthought.

func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }
    // ...
}

The Select Loop

When you need to coordinate multiple channels, select is your friend. A common pattern:

for {
    select {
    case msg := <-messages:
        handle(msg)
    case <-ctx.Done():
        return ctx.Err()
    case <-ticker.C:
        flush()
    }
}

Things I Wish I Knew Earlier

  1. Channel ownership — the goroutine that writes should also close the channel
  2. Buffered channels aren’t a fix for deadlocks — they just push the problem further out
  3. sync.WaitGroup copies by value — pass it by pointer, always
  4. The race detector is freego test -race should be part of your CI

Concurrency in Go is easy to write but hard to get right. The compiler won’t save you from logical races. That’s on us.