go.goroutine_leak
Stability
High
Causes Production Outages
Detects goroutines that may never terminate (blocking on channels without cancellation).
Why It Matters
Section titled “Why It Matters”Leaked goroutines accumulate over time:
- Memory growth - Each goroutine uses ~2KB stack minimum
- Resource exhaustion - Eventually crashes the application
- Hard to detect - No obvious symptoms until it’s too late
- Slow degradation - Performance degrades gradually
A goroutine leak is like a memory leak, but worse because goroutines also hold references to other resources.
Example
Section titled “Example”// ❌ Beforefunc startWorker() { go func() { for { msg := <-messages // Blocks forever if channel closes process(msg) } }()}If messages is never closed and nothing sends, this goroutine lives forever.
// ✅ Afterfunc startWorker(ctx context.Context) { go func() { for { select { case msg := <-messages: process(msg) case <-ctx.Done(): return } } }()}The goroutine exits when context is cancelled.
What Unfault Detects
Section titled “What Unfault Detects”- Goroutines blocking on channel receive without
select - Goroutines without cancellation mechanism
- Channel sends without corresponding receives
- Forever loops without exit conditions
Auto-Fix
Section titled “Auto-Fix”Unfault can add context-based cancellation to goroutine patterns when the blocking pattern is clearly identified.
Common Leak Patterns
Section titled “Common Leak Patterns”// LEAK: Unbuffered channel with no receiverch := make(chan int)go func() { ch <- 1 // Blocks forever}()
// LEAK: Range over channel never closedgo func() { for v := range ch { // Blocks forever at end process(v) }}()
// LEAK: Select without done casego func() { select { case v := <-ch: process(v) } // Only processes once, then exits - but what if ch never sends?}()Safe Patterns
Section titled “Safe Patterns”// Timeoutselect {case v := <-ch: process(v)case <-time.After(30 * time.Second): return // Give up after timeout}
// Context cancellationselect {case v := <-ch: process(v)case <-ctx.Done(): return // Cancelled by parent}
// Buffered channels for fire-and-forgetch := make(chan int, 1)ch <- 1 // Doesn't block