context
context is named as context, and its original design purpose is to transmit signals and some simple data across multiple goroutines, especially between parent and child goroutines. It's commonly used in scenarios such as handling HTTP requests, task scheduling, database queries, etc. Especially in microservice architectures, gRPC uses context for cross-process and cross-network metadata transmission, link control operations, etc.
package main
import (
"context"
"fmt"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
defer stop()
for {
select {
case <-ctx.Done():
fmt.Println("terminate")
return
default:
}
fmt.Println("running")
time.Sleep(100 * time.Millisecond)
}
}In the above example, process signals are transmitted through context. When a signal is received, the program exits itself. This is also an application scenario of context.
Structure
context.Context is not a concrete implementation, but an interface defining a set of methods:
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline(), returns a deadline and a boolean indicating whether it has endedDone(), returns a channel used to notify end messagesErr(), returns the reason for context closureValue(), gets the specified value according to key
There are the following usable contexts in the standard library:
Background, blank context, usually used as the root node of contextsWithCancel,WithCancelCause, cancellable contextWithDeadline,WithDeadlineCause, context with deadlineWithTimeout,WithTimeoutCause, context with timeoutWithValue, context that can carry values
Specifically, the main implementations are:
timerCtxcancelCtxemptyCtx
So its core functionality has four points:
- Cancel
- Deadline
- Value passing
- Propagation
Understanding these points basically clarifies how context works.
Cancel
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
cause error // set to non-nil by the first cancel call
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}The core of cancelCtx lies in the propagateCancel method, which is responsible for propagating the cancellable behavior to parent and child contexts.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}First, it checks whether the parent context can be canceled. If not, it returns directly.
godone := parent.Done() if done == nil { return // parent is never canceled }Returns and checks whether the parent context has already been canceled. If yes, it cancels all child contexts.
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }Tries to convert the parent context to
cancelCtxtype. If successful, it adds the current context to the parent context'schildren.goif p, ok := parentCancelCtx(parent); ok { // parent is a *cancelCtx, or derives from one. p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err, p.cause) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() return }Tries to convert it to
afterFuncertype. If successful, it registers the method for canceling the current context to the parent context'sAfterFunc.goif a, ok := parent.(afterFuncer); ok { // parent implements an AfterFunc method. c.mu.Lock() stop := a.AfterFunc(func() { child.cancel(false, parent.Err(), Cause(parent)) }) c.Context = stopCtx{ Context: parent, stop: stop, } c.mu.Unlock() return }If still not working, it starts a separate goroutine to listen to the
Donechannel. When a signal is received, it cancels the child context.gogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
Then the cancelCtx.cancel method is ultimately responsible for canceling child contexts:
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}Its flow is as follows:
Check whether the current context has already been canceled.
goif err == nil { panic("context: internal error: missing cancel error") } if cause == nil { cause = err } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled }Close the
donechannel and send closure notification.goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Traverse and notify child contexts.
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()Finally, determine whether to remove from the parent context based on the passed parameter.
goif removeFromParent { removeChild(c.Context, c) }
Deadline
WithTimeout and WithDeadline are both contexts with deadlines. They are the same type, just with different semantics, and both are based on cancelCtx.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause is responsible for creating contexts with deadlines:
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Its flow is as follows:
Check the deadline. If the parent deadline is earlier than the current deadline, then the parent context will definitely be canceled before the current context, so directly create a
cancelCtxtype context.goif parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) }Build
timerCtxand propagate to child contexts.goc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Calculate the current deadline. If already expired, cancel directly.
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }If not expired, use
time.AfterFuncto set canceling the current context at the deadline.goc.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded, cause) }) } return c, func() { c.cancel(true, Canceled, nil) }
For timerCtx, its method for canceling the context just stops the timer and also stops child contexts.
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}Value Passing
valueCtx can pass and get values in contexts:
type valueCtx struct {
Context
key, val any
}
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}Its core lies in the value function. Other context concrete implementations also enter this function to get values, such as:
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}The value function is a large for loop that continuously recurses upward to find the specified key's value:
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}Handling of different Context types:
*valueCtx: Ifkeymatches the current Context'skey, return thevalcorresponding tokey.*cancelCtx: IfkeymatchescancelCtxKey, return the currentcancelCtx, meaning return itself.withoutCancelCtx: Represents a Context without cancel functionality. IfkeymatchescancelCtxKey, returnnil.*timerCtx: IfkeymatchescancelCtxKey, return its associatedcancelCtx.backgroundCtxandtodoCtx: Usually special types of Contexts that don't carry any extra values. When encountering these two types, returnnildirectly.- If unknown type, continue calling
Valuemethod to search.
Summary
The core of these contexts is cancelCtx, which propagates cancel signals. Other context types and even third-party context types are layered on top of this, implementing various functionalities based on this foundation.
