How do goroutine leaks happen, and how do you detect them?
Question 361MediumGo 1.22 to 1.25
A goroutine blocked forever is never collected, and neither is everything its stack references. Goroutine leaks are the most common real-world Go memory leak.
// LEAK: returns after the first result; the others block on send forever
func firstLeaky(urls []string) string {
ch := make(chan string)
for _, u := range urls {
go func() { ch <- fetch(u) }()
}
return <-ch
}
// FIX: buffered channel sized to the number of senders (or use ctx cancel)
func first(ctx context.Context, urls []string) string {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // tells the stragglers to stop
ch := make(chan string, len(urls))
for _, u := range urls {
go func() { ch <- fetchCtx(ctx, u) }()
}
return <-ch
}
Typical causes: sending with no receiver, receiving from a channel that is never closed, a range over a channel whose producer never calls close, missing ctx.Done() cases, sync.WaitGroup counts that never reach zero, HTTP response bodies left unclosed, and forgotten time.Ticker loops.
Detection:
- Watch
runtime.NumGoroutine()or the/sched/goroutines:goroutinesmetric over time. - Open
/debug/pprof/goroutine?debug=1to see goroutines grouped by stack. A group whose count keeps growing is the leak. - In tests, call
goleak.VerifyNone(t)from go.uber.org/goleak. - Go 1.26 adds an experimental goroutine leak profile (
GOEXPERIMENT=goroutineleakprofile). It uses the GC to find goroutines blocked on concurrency primitives that nothing else can reach.
More on Memory, GC & Runtime Internals
- Q359Why can removing elements from a slice of pointers leak memory?
- Q360Do Go maps shrink after you delete entries? How do you reclaim the memory?
- Q362Is
time.Afterin a loop a memory leak? What changed in Go 1.23? - Q363How do you find a memory leak in production with pprof?
- Q364How does GOMAXPROCS interact with containers, and how does it affect GC?
- Q365What are the runtime representations of Go's built-in types? What does this print on 64-bit?