Spot the goroutine leak.
Question 241MediumGo 1.22 to 1.25
func fastest(ctx context.Context, mirrors []string) string {
ch := make(chan string)
for _, m := range mirrors {
go func() { ch <- fetch(ctx, m) }()
}
return <-ch
}
Only the first result is received. The other len(mirrors)-1 goroutines block forever on the unbuffered send, and each one pins its stack and the response memory. Called once per request, this steadily leaks memory and goroutines.
Fixes:
func fastest(ctx context.Context, mirrors []string) string {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // tell the losers to stop working
ch := make(chan string, len(mirrors)) // every sender can complete
for _, m := range mirrors {
go func() { ch <- fetch(ctx, m) }()
}
return <-ch
}
- A buffer of
len(mirrors)guarantees that no sender ever blocks. - Cancelling the context makes the losing fetches stop early instead of finishing work nobody will read.
- An alternative is a
selectwithdefaultin the sender, dropping the result when nobody is listening.
The general rule: for every goroutine you start, know exactly how it will exit.
More on Concurrency Patterns & sync
- Q239How do nil channels behave in select, and how are they used to merge two channels?
- Q240Build a cancellable pipeline. What causes goroutine leaks in pipelines and how do you prevent them?
- Q242How do you close a channel safely when there are multiple senders?
- Q243How do you implement a semaphore in Go? Compare a buffered channel with golang.org/x/sync/semaphore.
- Q244Explain errgroup: WithContext, SetLimit, TryGo. What are its semantics and gotchas?
- Q245You need to process 10 million records from a file with bounded memory and bounded parallelism. How do you design it?