Go

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

All 38 Channels & select questions