Introduction
A goroutine leak occurs when a goroutine is started but never terminates. Leaked goroutines silently consume memory and can eventually crash a long-running service. Learning to prevent them is essential for production Go code.
Key Concepts
- Goroutine Leak: A goroutine that is blocked forever or running an infinite loop without an exit path.
- Blocked Send/Receive: A channel operation that will never complete because there is no corresponding receiver or sender.
- Nil Channel: Sending to or receiving from a
nilchannel blocks forever.
Real World Context
In a microservice handling thousands of requests per second, even a single leaked goroutine per request means tens of thousands of leaked goroutines per hour. Monitoring runtime.NumGoroutine() in production dashboards is a standard practice to catch leaks before they cause OOM kills.
Deep Dive
Common Causes
- Blocked Send: Sending to a channel that has no receiver.
- Blocked Receive: Waiting on a channel that will never send or close.
- Nil Channel: Sending or receiving from a nil channel blocks forever.
- Infinite Loop: A loop without exit condition or proper cancellation.
Detect leaks by monitoring the goroutine count:
gofmt.Println("Active goroutines:", runtime.NumGoroutine())
Solutions
- Always ensure there is a path for the goroutine to exit.
- Use
context.Contextfor cancellation. - Close channels when done sending.
- Use buffered channels when a sender should not block.
Here is a common leak scenario and its fix:
go// LEAK: nothing ever sends to ch func leak() { ch := make(chan int) go func() { val := <-ch // Blocks forever fmt.Println(val) }() } // FIXED: use context for cancellation func noLeak(ctx context.Context) { ch := make(chan int) go func() { select { case val := <-ch: fmt.Println(val) case <-ctx.Done(): return } }() }
Common Pitfalls
- Forgetting to close channels — If a producer goroutine exits without closing its output channel, consumers block forever waiting for more data.
- Not using context for cancellation — Without a cancellation mechanism, background goroutines have no way to know when they should stop.
Best Practices
- Monitor goroutine count in production — Export
runtime.NumGoroutine()as a metric and alert on unexpected growth. - Use
goleakin tests — Thego.uber.org/goleakpackage detects goroutine leaks in unit tests.
Summary
- Goroutine leaks consume memory and can crash long-running services.
- Common causes: blocked channel operations, nil channels, infinite loops.
- Always provide an exit path via
context.Contextor channel closing. - Monitor
runtime.NumGoroutine()and usegoleakin tests.
Code Examples
go
func leak() {
ch := make(chan int)
go func() {
val := <-ch // Blocks forever because nothing sends to ch
fmt.Println(val)
}()
}
// The anonymous goroutine is leaked — it will never terminate.