NOTE
2.1 context
Goroutine context, deadlines, cancellation signals, request-scoped data, Context/canceler implementations, WithCancel/WithTimeout/WithValue, and references.
This is a historical learning note and may contain outdated or incomplete understanding.
1. context
The context of a goroutine. It passes context information between goroutines, including cancellation signals, timeout duration, deadlines, k-v values, etc. It is one way to control concurrency; another is sync.WaitGroup.md.
2. Usage
2.1. deadlines: timeout cancellation
func TestContext1(t *testing.T) {
// Set a context that times out after one second
d := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(2 * time.Second):
fmt.Println("timeout after 2s")
case <-ctx.Done():
// Timeout after 1s
fmt.Println(ctx.Err())
}
}
// Output
context deadline exceeded
2.2. cancellation signals: control termination of multiple goroutines
- A channel can only control one goroutine.
func TestChannel1(t *testing.T) {
stop := make(chan bool)
go func() {
for {
select {
case <-stop:
fmt.Println("monitor exits, stopped...")
return
default:
fmt.Println("goroutine keeps monitoring...")
time.Sleep(2 * time.Second)
}
}
}()
// Notify the goroutine to stop after 10s
time.Sleep(10 * time.Second)
fmt.Println("notify goroutine to stop monitoring")
stop<- true
time.Sleep(5 * time.Second)
}
// Output
goroutine keeps monitoring...
goroutine keeps monitoring...
goroutine keeps monitoring...
goroutine keeps monitoring...
goroutine keeps monitoring...
notify goroutine to stop monitoring
monitor exits, stopped...
- A context can control multiple goroutines.
func TestContext2(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "[monitor 1]")
go watch(ctx, "[monitor 2]")
go watch(ctx, "[monitor 3]")
// Call cancel after 10s to notify all goroutines to cancel
time.Sleep(10 * time.Second)
fmt.Println("notify goroutines to stop monitoring")
cancel()
// To check whether monitoring has stopped, no monitoring output means it has stopped
time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "monitor exits, stopped...", ctx.Err())
return
default:
fmt.Println(name, "goroutine keeps monitoring...")
time.Sleep(2 * time.Second)
}
}
}
// Output
[monitor 3] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 3] goroutine keeps monitoring...
[monitor 3] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 3] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 3] goroutine keeps monitoring...
notify goroutines to stop monitoring
[monitor 2] monitor exits, stopped... context canceled
[monitor 3] monitor exits, stopped... context canceled
[monitor 1] monitor exits, stopped... context canceled
2.3. request-scoped data: similar to ThreadLocal
func TestContext3(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
// Attach a value
valueCtx := context.WithValue(ctx, key, "[monitor 1]")
go watch2(valueCtx)
// Call cancel after 10s to notify all goroutines to cancel
time.Sleep(10 * time.Second)
fmt.Println("notify goroutines to stop monitoring")
cancel()
// To check whether monitoring has stopped, no monitoring output means it has stopped
time.Sleep(5 * time.Second)
}
func watch2(ctx context.Context) {
for {
select {
case <-ctx.Done():
// Get the value
fmt.Println(ctx.Value(key), "monitor exits, stopped...")
return
default:
// Get the value
fmt.Println(ctx.Value(key), "goroutine monitoring...")
time.Sleep(2 * time.Second)
}
}
}
// Output
[monitor 1] goroutine monitoring...
[monitor 1] goroutine monitoring...
[monitor 1] goroutine monitoring...
[monitor 1] goroutine monitoring...
[monitor 1] goroutine monitoring...
notify goroutines to stop monitoring
[monitor 1] monitor exits, stopped...
3. Internals
3.1. Class Diagram

3.2. Data Structures
3.2.1. Context Interface
All Context implementations implement this interface.
type Context interface {
// When the context is canceled or reaches its deadline, return a closed channel
// This is a receive-only channel. Therefore, a child goroutine cannot read anything from this channel unless it is closed. This is exactly what is used here: after a child goroutine reads a value (the zero value) from the channel, it can do cleanup work and exit as soon as possible.
Done() <-chan struct{}
// After channel Done is closed, return the reason the context was canceled, such as cancellation or timeout.
Err() error
// Return whether the context will be canceled and its automatic cancellation time (deadline)
Deadline() (deadline time.Time, ok bool)
// Get the value corresponding to key
Value(key interface{}) interface{}
}
3.2.1.1. emptyCtx
The default implementation of the Context interface.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
This emptyCtx is used to define background and todo. background is generally used in the main function, while todo is generally used as a placeholder.
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
3.2.1.2. valueCtx
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
3.2.2. canceler Interface
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
If a Context implements the canceler interface, it is cancelable, such as *cancelCtx and *timerCtx in the diagram above.
3.2.2.1. cancelCtx
Implements both the Context and canceler interfaces.
type cancelCtx struct {
// Implements Context
Context
// Protect the fields below
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
// Lazy creation: only create it when Done() is called for the first time
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
// Return the channel directly. Callers generally use it with select. Once it is closed, the zero value can be read
return d
}
// When canceling, the first parameter determines whether this node should remove itself from its parent. The second parameter is a fixed cancellation error type
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// err must be provided
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
// Already canceled by another goroutine
if c.err != nil {
c.mu.Unlock()
return
}
// Assign the err field
c.err = err
// Close the channel to notify other goroutines
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// Traverse all child nodes
for child := range c.children {
// Recursively cancel all child nodes
child.cancel(false, err)
}
// Clear the child nodes
c.children = nil
c.mu.Unlock()
if removeFromParent {
// Remove itself from the parent node
removeChild(c.Context, c)
}
}
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
// Delete this node from all child nodes of the parent
delete(p.children, child)
}
p.mu.Unlock()
}
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
// Only three Context types are recognized here: *cancelCtx, *timerCtx, and *valueCtx. If Context is embedded in another type, it cannot be recognized
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}

3.2.2.2. timerCtx
type timerCtx struct {
cancelCtx
// Based on cancelCtx, with only one additional time.Timer and one deadline
timer *time.Timer
deadline time.Time
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// Directly call cancelCtx's cancel method
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove the child node from the parent
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
// Stop the timer so that it will not cancel again when the deadline arrives
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
3.3. Methods
3.3.1. WithCancel
Creates a cancelCtx.
var Canceled = errors.New("context canceled")
// Pass in a parent Context (usually a background context as the root node)
// Return a new context and cancelFunc. When cancelFunc is called, or the parent node's CancelFunc is called, this context will be canceled
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
// Attach this node to the parent node
propagateCancel(parent, &c)
// Eventually call cancel
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
func propagateCancel(parent Context, child canceler) {
// The parent node is an empty node
if parent.Done() == nil {
return // parent is never canceled
}
// Find a cancelable parent context
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// The parent has already been canceled, so this node (child) must also be canceled
child.cancel(false, p.err)
} else {
// The parent has not been canceled
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// "Attach" to the parent
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// If no cancelable parent context was found, start a new goroutine to monitor the parent or child cancellation signal
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
3.3.2. WithTimeout
Creates a timerCtx.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
// If the parent context's deadline is earlier than the specified time, directly construct a cancelable context.
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// The reason is that once the parent times out, it automatically calls cancel, and the child will also be canceled.
// Therefore there is no need to separately handle the child timer automatically calling cancel after its own time expires.
return WithCancel(parent)
}
// Construct timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
// Attach to the parent
propagateCancel(parent, c)
// Calculate the time from now until the deadline
d := time.Until(deadline)
if d <= 0 {
// Cancel directly
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// After d time, timer automatically calls cancel. Automatic cancellation
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
3.3.3. WithValue
Creates a valueCtx.
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
Calling it multiple times constructs a tree.

When retrieving a value, first look in the current node; if it is not found, look in the parent node.
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
4. context canceled vs context deadline exceeded
Assume a context has a 1s timeout. Calling context.Cancel() triggers the context canceled error (active cancellation).
If more than 1s passes, it triggers the context deadline exceeded error (passive cancellation).
5. Summary
- Thread-safe.
- Context can be used for deadlines, cancellation signals, and passing data in other request scopes.
WithCancel,WithDeadline, andWithTimeoutaccept a parent context and return a child context plus aCancelFunc. CallingCancelFunccancels the child and its children, and removes the reference from the parent to this child. If it is not called, the child is canceled only when the parent is canceled.- Do not put
Contextin a struct. Share it as the first parameter of a function. When the correct context is uncertain, do not passnil; usecontext.TODO. - There are three main methods:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc): callingCancelFunccan release resources held by the context; otherwise wait for the parent to cancel it.func WithDeadline(parent Context, d time.Time) (Context, CancelFunc): adds a timeout; resources are automatically released when timedis reached.func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)isWithDeadline(parent, time.Now().Add(timeout)).
func WithValue(parent Context, key, val interface{}) Context:keymust be comparable and cannot be a built-in type.- In simple terms, ctx is a linked list, with O(N) efficiency.
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub