| name | go-goroutine-leaks |
| description | Prevent goroutine leaks with proper shutdown mechanisms |
Goroutine Leak Prevention
Pattern
Every goroutine must have a way to exit. Use channels or context for shutdown signals.
CORRECT - Done channel
type Worker struct {
done chan struct{}
}
func (w *Worker) Start() {
w.done = make(chan struct{})
go func() {
for {
select {
case <-w.done:
return
case <-time.After(1 * time.Second):
}
}
}()
}
func (w *Worker) Stop() {
close(w.done)
}
CORRECT - Context
func StartWorker(ctx context.Context) {
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}()
}
WRONG - No exit mechanism
func StartWorker() {
go func() {
for {
time.Sleep(1 * time.Second)
}
}()
}
WRONG - Unbuffered channel send can block
func GetData() string {
ch := make(chan string)
go func() {
ch <- fetchData()
}()
select {
case result := <-ch:
return result
case <-time.After(1 * time.Second):
return "timeout"
}
}
Fix with buffered channel
func GetData() string {
ch := make(chan string, 1)
go func() {
ch <- fetchData()
}()
select {
case result := <-ch:
return result
case <-time.After(1 * time.Second):
return "timeout"
}
}
Rules
- Every
go func() needs an exit condition
- Use
select with ctx.Done() or done channel
- Buffered channels (size 1) for single sends
- Close channels to signal completion
- Test with
runtime.NumGoroutine() to detect leaks
Detection
func TestNoLeaks(t *testing.T) {
before := runtime.NumGoroutine()
worker := NewWorker()
worker.Start()
worker.Stop()
time.Sleep(100 * time.Millisecond)
after := runtime.NumGoroutine()
if after > before {
t.Errorf("goroutine leak: before=%d after=%d", before, after)
}
}