How do you detect and prevent goroutine leaks?

5 minadvancedgogoroutinesleaksconcurrency

Quick Answer

A goroutine leak happens when a goroutine blocks forever, usually on a channel operation that will never complete, and never gets cleaned up — the goroutine (and everything it's holding onto) stays alive for the life of the program. Common causes: sending to a channel nobody is reading from anymore, or waiting on a channel that never gets closed or written to after its producer already gave up. Prevent leaks by always giving a goroutine a way to exit, typically a context.Context it selects on alongside its main work, and by ensuring every channel has a clear owner responsible for closing it. Detect leaks with the -race detector for related bugs, runtime.NumGoroutine() to watch goroutine counts over time in tests, or dedicated tools like go.uber.org/goleak in test suites.

Detailed Answer

A goroutine leak doesn't crash your program immediately. It just quietly accumulates blocked goroutines until memory or other resources run out.

A classic leak

func startWorker(dataCh chan int) {
    go func() {
        for {
            v := <-dataCh  // blocks forever if nobody ever sends again, and nobody closes it
            process(v)
        }
    }()
}

If the caller stops sending to dataCh and never closes it, this goroutine blocks on the receive forever. It's never garbage collected, since it's still technically runnable, just permanently parked.

Fixing it with context cancellation

func startWorker(ctx context.Context, dataCh chan int) {
    go func() {
        for {
            select {
            case <-ctx.Done():
                return   // clean exit when the caller cancels
            case v := <-dataCh:
                process(v)
            }
        }
    }()
}

Detecting leaks

import "go.uber.org/goleak"

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)  // fails the test suite if goroutines are still running after tests finish
}

goleak snapshots running goroutines before and after a test and fails if new ones are still alive at the end, which catches leaks that are otherwise invisible until they show up as a slow memory creep in production.

The general rule

Every goroutine you start should have a clear, reachable path to termination. If you can't answer "what makes this goroutine return" when you write the go statement, that's a strong signal you're about to leak one.