There are a number of under-the-hood implementations of the Context interface in the Go standard library. For instance, the Background and TODO contexts are backed by the unexposed emptyCtx type which is essentially just int with some stub methods (proof). Similarly, every call to context.WithCancel() returns an instance of the cancelCtx type which is already a proper struct with a bunch of mutex-protected attributes (proof):
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
Why does the cancelCtx struct use a mutually exclusive lock and not an RWLock? For instance, the Err() method currently acquires a full lock while it (probably) could have used just an RLock:
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
One reason should be
RWLockhas poor performance.The performance of locks doesn't depends on the features it provides, it depends on the underlying
implementation. Although theoreticallyRWLockcan provides higherthroughputs, for this specific scenario (mutating a tiny variable),Mutexcould provide lowerunnecessary overhead.