Back to Blog
GolangConcurrencyBackendPerformance

Concurrency Patterns in Go You Should Actually Use in Production

April 20, 20268 min read

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.

go
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

go
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

go
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