Back to Blog
GolangConcurrencyBackendPerformance

Concurrency Patterns in Go You Should Actually Use in Production

8 min read · April 20, 2026

Why Most Go Concurrency Tutorials Fail You

Tutorials show you goroutines and channels. Production shows you goroutine leaks at 3 AM. Here are the patterns that actually matter when you're running high-throughput services.


1. Always Use Context for Cancellation

Never launch a goroutine without a cancellation path.

func processStream(ctx context.Context, stream <-chan Event) error { for { select { case <-ctx.Done(): return ctx.Err() case event, ok := <-stream: if !ok { return nil } if err := handle(event); err != nil { return fmt.Errorf("handle event: %w", err) } } } }

2. Worker Pool — The Right Way

func workerPool(ctx context.Context, jobs <-chan Job, concurrency int) error { g, ctx := errgroup.WithContext(ctx) for range concurrency { g.Go(func() error { for { select { case <-ctx.Done(): return ctx.Err() case job, ok := <-jobs: if !ok { return nil } if err := job.Process(ctx); err != nil { return err } } } }) } return g.Wait() }

3. Semaphore for Resource Limiting

type Semaphore chan struct{} func NewSemaphore(n int) Semaphore { return make(chan struct{}, n) } func (s Semaphore) Acquire(ctx context.Context) error { select { case s <- struct{}{}: return nil case <-ctx.Done(): return ctx.Err() } } func (s Semaphore) Release() { <-s }

Key Takeaways

  • Every goroutine needs an exit condition
  • errgroup > sync.WaitGroup for error propagation
  • Bounded pools > unbounded goroutines
  • Test for leaks with go.uber.org/goleak