Go Concurrency Patterns: Goroutines, Channels, and Beyond

Master Go's concurrency model — goroutines, channels, sync primitives, and production-ready patterns for building concurrent systems.

Go Concurrency Patterns: Goroutines, Channels, and Beyond

Go’s concurrency model is one of its greatest strengths. Goroutines are cheap, channels make communication clean, and the standard library gives you everything you need. But getting it right requires understanding the patterns. Let’s dig in.

Goroutines: Lightweight Threads

A goroutine is a function running concurrently with other goroutines in the same address space. They start with ~8KB of stack and grow as needed.

In other languages, 10,000 threads would exhaust memory. In Go, this is fine.

Channels: Communication Between Goroutines

“Do not communicate by sharing memory; instead, share memory by communicating.” — Go Proverb

Pattern 1: Worker Pool

Limit concurrency to avoid overwhelming downstream systems:

Pattern 2: Fan-Out, Fan-In

Distribute work across multiple goroutines, then merge results:

Pattern 3: Context for Cancellation

Always propagate context for cancellation and deadlines:

Pattern 4: errgroup for Parallel Operations

Run multiple operations concurrently and collect errors:

This runs all three queries in parallel, cutting 3 sequential queries (e.g., 3 × 50ms = 150ms) down to max(50ms, 50ms, 50ms) = ~50ms.

sync.Mutex vs sync.RWMutex

Use sync.RWMutex when reads are much more frequent than writes.

sync.Once for Initialization

Common Mistakes

Goroutine Leak

Always ensure goroutines can exit:

Data Race

Use go test -race to detect data races:

Closing a Channel Twice

Go’s concurrency model is powerful but requires discipline. Use -race in tests, always propagate context, and design goroutine lifecycles explicitly.