Explanation
What just happened
select waited for whichever came first: the result or time.After(50ms). The timeout won and main moved on. But nothing stopped slowCall. It finished its call and tried to send on result, an unbuffered channel nobody would ever read again. So it waited forever in [chan send].
runtime.NumGoroutine() said 2 long after the timeout. That goroutine leaked.
Try the what-if switch
- buffer of 1: the late send drops into the buffer, so
slowCallreturns and the leak is gone. It still does the whole slow call for nobody. - context:
slowCallselects onctx.Done()too. When the deadline passes, both selects wake.slowCallreturns without sending, andmainprintstimed out: context deadline exceeded.
Why
Go has no way to kill a goroutine. It stops only by returning, so it has to know when to stop. That’s what a context is for.
Watch out
Cancelling a context stops nothing by itself. It only helps code that checks ctx.Done().
Common questions
Does a select timeout stop the goroutine?
No. The timeout only ends the wait in select. The other goroutine keeps running, and if it then sends on an unbuffered channel nobody reads, it's stuck in [chan send] forever.
How do I cancel a goroutine in Go?
You can't stop it from outside. Pass a context.Context and have the goroutine select on ctx.Done(), so it returns when the caller gives up.
What does ctx.Err() return after a timeout?
context deadline exceeded for a context made with context.WithTimeout whose time ran out.
Verified: this timeline matches a real program, checked in CI on Go 1.26 and 1.27. How we verify. Unclear or wrong? Tell us