First, to answer the puzzle left by the previous article’s quick start context, why defer cancelFunc()?

func main() { parent := context.Background() for i := 0; i < 100; I++ {go doRequest(parent)} time.sleep (time.second * 10)} func doRequest(parent context.context) {  ctx, _ := context.WithTimeout(parent, time.Second*5) time.Sleep(time.Millisecond * 200) go func() { <-ctx.Done() fmt.Println("ctx done!" ()})}

See the code above, call DoRequest asynchronously in main function, create a new 5S timeout context in DoRequest function, and the call time of DoRequest is 200ms

<img src=”” alt=”image-20210710222940035″ style=”zoom:50%;” />

As you can see, the context time range of doRequest is much larger than the time taken by the function call, and the context is not actively canceled after the function ends, which can cause context leakage

So,defer cancelFunc()The purpose is to avoid context disclosure!!

Proactively calling cancelFunc is a good habit!

Take a look at the Context interface

Type Context interface {// [1] return the Deadline() (Deadline time.time, OK bool) // return a channel that will be closed when the Context ends, Struct () <-chan struct{} // [2] This method returns a not nil Err at the end of the context. Err() error // return the value of the context associated with the key value (key interface{}) interface{}}

[1] When the context does not set a Deadline, call Deadline and return the result value with OK = false

func main() { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() deadline, Printf(" OK = %v, Deadline = %v\n", OK, Deadline) // output OK = false, deadline = 0001-01-01 00:00:00 +0000 UTC }

[2], even if the context is actively removed, Err returns the value not nil

CancelFunc := Context.withTimeout (context.background (), cancelFunc := Context.withTimeout (context.background (), CancelFunc ()}() < -ctx.done () err := ctx.err (); fmt.Printf("err == nil ? %v\n", err == nil) // Output err == nil? false }

There are a few structures you can’t miss

After looking at the Context interface, let’s take a look at the four context-corresponding structures predefined in the Context package

<img src=”” alt=”image-20210710231150954″ style=”zoom:50%;” />

As you can see, there are three structures for each of the four contexts, and TimerctX is the underlying use for both the timeout context and the cut-off context

Next, let’s look at what properties are in each of the three constructs and how they implement the Context interface


Type cancelCtx struct{Context // [1] MU sync.mutex // This lock is used to protect the following fields: Canceler struct{} err error} // create cancelable Context func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: Parent}} func (c *cancelCtx) Value(key interface{}) interface{} {if key == &cancelCtxKey {return c} // Search for the Value of the parent context Return c.ontext.value (key)} func (c *cancelCtx) Done() <-chan struct{} { () if == nil {// The first time I call the Done method, C.done = make(chan struct{})} d := c.done () return d} func (c *cancelCtx) Err() error { err := c.err return err }

At [1], you can see that CancelCTX has an anonymous interface embedded

When the CancelCTX structure is built, the parent context Parent is used as the implementation of the anonymous interface of the structure

At the same time, the structure overrides three methods in the anonymous interface, namely Value, Done, and Err

So, when you call the Deadline method in cancelCtx, you’re actually calling the parent’s Deadline method

[2] The done channel in the structure that indicates the end of the context is initialized lazily. The done channel is initialized the first time the done method is called


Func (c *timerCtx, struct {cancelCtx, struct {cancelCtx, struct {cancelCtx, struct {cancelCtx, struct} func (c *timerCtx, struct {cancelCtx, struct}); Deadline() (deadline time.Time, ok bool) { return c.deadline, true } func WithTimeout(parent Context, Timeout time.duration) (Context, cancelFunc) {return withDeadline (parent, cancelFunc); time.Now().Add(timeout)) } func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); Before(d) {return withCancel (parent)} c := &timerctx {cancelCtx: = &timerctx {cancelCtx: = % timerCtx{cancelCtx: = % timerCtx{cancelCtx: = % timerCtx{cancelCtx: = % timerCtx; newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Departing)}} () defer () if c.rr == nil {c.imer = time.afterfunc (dur, func() {// C. Cancel (true, DeadlineExceeded)})} return c, func() {C. Cancel (true, Canceled)}}

As you can see in [1], TimerctX has a cancelCTX structure embedded in it, so when TimerctX is built, it also accepts the parent context parent as the implementation of its embedded interface, and TimerctX only overrides the Deadline method

As can be seen in [2], the nature of the control of the cut-off time of the context is controlled by the Timer timer, and the context is cancelled at the specified time through Timer.afterFunc


type valueCtx struct { Context key, val interface{} } func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return C.ontext.value (key)} func WithValue(parent Context, key, parent Context) Val interface{}) Context {if key == nil {panic("nil key")}} // reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val} }

ValuecTx as a whole is simpler with the first two constructs, and the parent context Parent only overrides the Value method

The main concern is at [1], what types are not comparable?

  • slice
  • map
  • func

The three types are not comparable, and using a slice, map, or function as the key to valueCtx causes panic!!

Consider the following questions

Why do channels in context load lazily?

My guess is to save memory

First, either the active cancellation or the timed termination of the context is called to the cancel function

This function determines if the context channel is empty. If so, a global variable called CLOSEDCHAN is used. This channel is closed during package initialization

// This is a reusable channel var closedChan = make(Chan struct{}) func init() {// This is a reusable channel. Close channel close(closedchan)} func (c *cancelCtx) cancel(removeFromparent bool, err error) {.... Closedchan} else {close(}.... If == nil {// If done is empty, no done method has been called yet, use closechan instead of = closedchan} else {close(}.... }

The use of Context does not always require a call to the context.done method

With reusable ClosedChan, you avoid initializing the done channel immediately during the context building process, reducing some unnecessary memory allocation

What happens when cancelFunc is called multiple times?

No, no, no, no, no, no, no, no. Right

When cancelFunc is called, the underlying function calls cancel, which determines whether the current context has ended, and returns if it has

func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } if c.err ! = nil {// Err is not empty, which means that the context has been removed, and ends the process C.MU.U.Lock () return}... }

<img src=”” alt=”img” />

Can the cutoff time of the current context exceed that of the parent context?

No, the cutoff time of the context is the same as the cutoff time of the parent context

As you can see in the withDeadline function, the first step verifies the deadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); Before(d) {// Construct the cancellable context and return WithCancel(parent)}.... when the cutover time of the child context exceeds the parent context }

When a cancellable context is returned, the cutoff time of the child context is the same as that of the parent context

What is the difference between background and todo?

There is no difference in nature, the underlying structure is all constructed using emptyCtx, but the main difference is in the use of semantics

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)

func Background() Context {
    return background

func TODO() Context {
    return todo

When you’re not sure what context to pass, choose TODO, but usually this is a temporary situation, right