gotchagoMajorpending
Gotcha: Go goroutine leak from unbuffered channels
Viewed 0 times
goroutineleakchannelunbufferedcontextcancel
Error Messages
Problem
Goroutines sending to unbuffered channels block forever if no receiver is reading, causing goroutine leaks and memory growth.
Solution
Ensure all channel sends have corresponding receives:
// BAD - goroutine leaks if nobody reads:
func search(query string) chan Result {
ch := make(chan Result)
go func() {
result := doSearch(query)
ch <- result // Blocks forever if caller abandons!
}()
return ch
}
// If caller ignores the channel, goroutine leaks
// GOOD - use context for cancellation:
func search(ctx context.Context, query string) chan Result {
ch := make(chan Result, 1) // Buffered! Won't block
go func() {
result := doSearch(query)
select {
case ch <- result:
case <-ctx.Done(): // Cancelled, don't block
}
}()
return ch
}
// GOOD - use buffered channel of size 1:
ch := make(chan Result, 1) // Can write without receiver
// Detect goroutine leaks in tests:
import "runtime"
func TestNoLeak(t *testing.T) {
before := runtime.NumGoroutine()
// ... run test ...
time.Sleep(100 * time.Millisecond)
after := runtime.NumGoroutine()
if after > before {
t.Errorf("goroutine leak: %d -> %d", before, after)
}
}
// Or use goleak library:
// import "go.uber.org/goleak"
// defer goleak.VerifyNone(t)
// BAD - goroutine leaks if nobody reads:
func search(query string) chan Result {
ch := make(chan Result)
go func() {
result := doSearch(query)
ch <- result // Blocks forever if caller abandons!
}()
return ch
}
// If caller ignores the channel, goroutine leaks
// GOOD - use context for cancellation:
func search(ctx context.Context, query string) chan Result {
ch := make(chan Result, 1) // Buffered! Won't block
go func() {
result := doSearch(query)
select {
case ch <- result:
case <-ctx.Done(): // Cancelled, don't block
}
}()
return ch
}
// GOOD - use buffered channel of size 1:
ch := make(chan Result, 1) // Can write without receiver
// Detect goroutine leaks in tests:
import "runtime"
func TestNoLeak(t *testing.T) {
before := runtime.NumGoroutine()
// ... run test ...
time.Sleep(100 * time.Millisecond)
after := runtime.NumGoroutine()
if after > before {
t.Errorf("goroutine leak: %d -> %d", before, after)
}
}
// Or use goleak library:
// import "go.uber.org/goleak"
// defer goleak.VerifyNone(t)
Why
Goroutines are lightweight but still consume memory. A blocked goroutine never gets garbage collected, slowly exhausting resources.
Revisions (0)
No revisions yet.