Explanation
What just happened
All three workers got the same ctx. Between units of work, each one waited in a select for whichever came first: more work, or ctx.Done(). When main called cancel(), ctx.Done() was closed, and a closed channel wakes every goroutine waiting on it. All three reported and returned.
Try the what-if switch
- ignores ctx: the workers loop on
time.Sleepand never look atctx.cancel()changes nothing,mainwaits forever, and Go can’t even call it a deadlock: the workers aren’t stuck, just busy.
Why it works like this
Go has no way to kill a goroutine. Stopping is cooperative: the goroutine has to notice and return. A context is the standard way to pass that “please stop” down a call tree, and it also carries deadlines (WithTimeout).
Watch out
Check ctx.Done() often enough. A worker stuck in one long step without checking won’t notice the cancel until that step ends.
Common questions
How do I stop a goroutine in Go?
You can't stop it from outside. Give it a context.Context and have it return when ctx.Done() is closed, checked in a select between units of work.
Does cancelling a parent context cancel its children?
Yes. Every context made from a cancelled parent, with WithCancel, WithTimeout and the rest, is cancelled too, and its Err() is context canceled.
Why didn't my goroutine stop after cancel()?
Because its code never looks at ctx.Done(). cancel() only closes a channel; a goroutine that doesn't watch it keeps running.
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