Go

Code review: why does this timeout wrapper leak goroutines?

Question 501HardGo 1.22 to 1.25
func fetchWithTimeout(ctx context.Context) (string, error) {
    ch := make(chan string) // BUG: unbuffered
    go func() {
        ch <- slowFetch()    // blocks forever if nobody receives
    }()
    select {
    case v := <-ch:
        return v, nil
    case <-time.After(time.Second):
        return "", errors.New("timeout")
    }
}

When the timeout fires, the function returns and no one ever receives from ch. The goroutine stays blocked on the send forever, along with everything it references. Under load, that means thousands of leaked goroutines and growing memory.

Fixes:

func fetchWithTimeout(ctx context.Context) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()
    ch := make(chan string, 1) // buffered: the send never blocks
    go func() { ch <- slowFetchCtx(ctx) }()
    select {
    case v := <-ch:
        return v, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

The buffer of 1 lets the worker finish and exit. Passing ctx lets slowFetch actually stop its work instead of running to completion unused. Also point out that time.After in a hot loop allocates a timer each time. Since Go 1.23, unreferenced timers are garbage-collected, but context or a reused time.Timer is still cleaner. Catch leaks in tests with go.uber.org/goleak.

More on Tricky Output & Code-Review Puzzles

All 38 Tricky Output & Code-Review Puzzles questions