Is a context safe for concurrent use? What happens if you read ctx.Done() from many goroutines, or call cancel concurrently?
Question 288MediumGo 1.22 to 1.25
Yes. All Context methods and all cancel functions are safe to call from any number of goroutines. The rules:
Done()returns the same channel on every call. ForcancelCtxit is created lazily on first use (stored in anatomic.Value), so a context whoseDoneis never called never allocates a channel.- Cancellation closes that channel, and a closed channel wakes every receiver, current and future. This broadcast behavior is why Done is "close-only" and never sends a value.
- Concurrent calls to cancel are serialized by a mutex. The first one sets
errandcause; the rest do nothing.
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := range 3 {
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
fmt.Println("worker", i, "stopped:", ctx.Err())
}()
}
for range 3 {
go cancel() // racing cancels: safe
}
wg.Wait()
// Prints three "stopped: context canceled" lines, in arbitrary order.
What isn't automatically safe: the values you store. The context only gives you the pointer; any changes to the object behind it need their own synchronization.
More on Context
- Q286How do you implement graceful shutdown with signal.NotifyContext, and what context mistake defeats it?
- Q287How do you test context-aware code, including timeouts and cancellation?
- Q289How should a library expose context in its API, and how do you add context support to an existing API without breaking callers?
- Q290What does this print? (AfterFunc on already-canceled and live contexts)
- Q291What does this print? (Cause across WithoutCancel)
- Q292How do you make a range-over-func iterator (iter.Seq) respect context cancellation, and why is it better than a channel-based generator?