What is a goroutine leak caused by channels? Show one and fix it.
Question 205HardGo 1.22 to 1.25
// LEAKS on timeout
func fetch(url string) (string, error) {
ch := make(chan string) // unbuffered
go func() {
ch <- slowGet(url) // blocks forever if nobody receives
}()
select {
case s := <-ch:
return s, nil
case <-time.After(time.Second):
return "", errors.New("timeout") // goroutine stuck on send
}
}
When the timeout wins, nobody will ever receive from ch. The goroutine stays parked on the send forever, and it keeps its stack and everything it references. Blocked goroutines are GC roots, so they are never collected.
Fixes:
ch := make(chan string, 1) // Fix 1: buffer 1, the send always completes
// Fix 2 (better): propagate cancellation so the work itself stops
func fetch(ctx context.Context, url string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil { return "", err }
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
return string(b), err
}
Fix 1 stops the leak, but the wasted work still runs to completion. Fix 2 actually cancels the work. Catch leaks in tests with goleak.VerifyNone(t).
More on Channels & select
- Q203How would you implement a semaphore / bounded concurrency with channels?
- Q204Implement a worker pool with result collection and proper shutdown.
- Q206Why is
chan struct{}preferred for signaling, and how doescloseact as a broadcast? - Q207What does this print? (len and cap of channels)
- Q208Channels or mutexes: how do you decide?
- Q209Explain the pipeline pattern and how to cancel it properly.