Go Context explanation of the ultimate no doubt

1. What is Context

Go 1.7 standard library introduces Context, which is called Context in Chinese. It is an interface used to transfer deadline, cancellation signal and request related values across API s and processes.

context.Context It is defined as follows:

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

Deadline() returns a deadline for completing the work, indicating the time when the context should be cancelled. If ok==false, the deadline is not set.

When the Channel () is closed, it will return a context that the Channel () should be closed. If this context cannot be canceled, Done may return nil. Calling the Done method multiple times will return the same Channel.

Err() returns the reason for the end of the Context. It will only return a non null value when the Channel corresponding to the Done method is closed. If the Context is cancelled, the Context will be returned Cancelled error; If Context times out, it will return Context Deadlineexceeded error.

Value() gets the value corresponding to the key from the Context. If the value corresponding to the key is not set, nil is returned. Multiple calls with the same key will return the same result.

In addition, the context package provides two functions to create the default context:

// TODO returns a non nil but empty context.
// Context should be used when it is not clear which context to use or when no context is available TODO. 
func TODO() Context

// Background returns a non nil but empty context.
// It will not be cancel led, there is no value, and there is no deadline. It is usually used by main functions, initialization, and testing, and serves as the top-level context for processing requests.
func Background() Context

There are also four functions that create different types of contexts based on the parent:

// WithCancel creates a context with Done channel based on the parent
func WithCancel(parent Context) (Context, CancelFunc)

// WithDeadline creates a context that ends no later than d Based on the parent
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

// WithTimeout is equivalent to withdeadline (parent, time. Now() Add(timeout))
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

// WithValue creates a context containing the specified key and value based on the parent
func WithValue(parent Context, key, val interface{}) Context

The usage of these different types of context will be described in detail later.

2. Why is there a Context

Go is born for background services. If you only need a few lines of code, you can build an HTTP service.

In the Go service, usually every request will start several goroutine s to work at the same time: some execute business logic, some Go to the database to get data, and some call the downstream interface to get relevant data

Synergetic process a generates B, c, D, c, e, e, f. Parent processes and descendant processes are associated. They need to share the relevant information of the request, such as user login status, request timeout, etc. How to connect these collaborative processes, context came into being.

Then again, why link these processes together? Take timeout as an example. When the request is cancelled or the processing time is too long, it may be that the user has closed the browser or has exceeded the timeout specified by the requester, and the requester directly gives up the request result. At this time, all goroutines working for this request need to exit quickly because their "work products" are no longer needed. After all associated goroutines exit, the system can recycle relevant resources.

Generally speaking, the function of context is to transfer context information (cancel signal, deadline, request scoped value) between a group of goroutine s to achieve their management control.

3. List of source code of context package

The Go version we analyzed is still 1.17.

3.1 Context

Context is an interface. As long as a type implements all its declared methods, it implements context. Look at the definition of context again.

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

The function of the method has been described in detail above and will not be repeated here.

3.2 CancelFunc

In addition, a function type is defined in the context package CancelFunc

type CancelFunc func()

Cancelfunction notifies the operation to abandon its work. Cancelfund will not wait for the work to stop. Multiple goroutine s can call cancelfunction at the same time. Subsequent calls to cancelfunction do not do anything after the first call.

3.3 canceler

The context package also defines a simpler context for canceling, called canceller, which is defined as follows.

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
	cancel(removeFromParent bool, err error)
	Done() <-chan struct{}
}

Because its initial letter is lowercase, the interface is not exported and cannot be used directly by external packages. It is only used in the context package. The types that implement this interface include * cancelCtx and * timerCtx.

Why does one of the methods cancel() have a lowercase initial and is not exported, while Done() must be implemented for export? Why is it so designed?

(1) The "Cancel" operation should be recommended, not mandatory.
Callers should not care about or interfere with callee's situation. It is callee's responsibility to decide how and when to return. The caller only needs to send a "Cancel" message, and the callee makes further decisions based on the received information. Therefore, the interface does not define the cancel method.

(2) The cancel operation should be transitive.
When you cancel a function, other functions associated with it should also be cancelled. Therefore, the Done() method returns a read-only channel, and all related functions listen to this channel. Once the channel is closed, all listeners can receive it through the "broadcast mechanism" of the channel.

3.4 implementation of context

After the Context interface is defined in the Context package, four implementations are given:

  • emptyCtx
  • cancelCtx
  • timerCtx
  • valueCtx

We can choose to use different contexts according to different scenarios.

3.4.1 emptyCtx

emptyCtx, as its name suggests, is an empty context. It cannot be cancelled. It does not carry a value and has no deadline.

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}

It is not exported, but is packaged into the following two variables, which can be used externally through the corresponding export function.

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

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
	return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
	return todo
}

From the perspective of source code, Background() and TODO() return two empty context objects of the same type and different types respectively. There is no big difference, but there is a slight difference in usage and semantics:

  • Background() is the default value of context, from which all other contexts should be derived; For example, it is used in the main function or as the top-level context.
  • TODO() is usually used when you don't know what context to pass. If you call a function that needs to pass the context parameter, you can pass todo if you don't have a ready-made context to pass. This often happens when refactoring is in progress. When a context parameter is added to some functions, but you don't know what to pass, you use todo to "occupy a seat", and finally change to another context.

3.4.2 cancelCtx

cancelCtx is a Context used to cancel the operation and implements the canceler interface. It directly takes the interface Context as an anonymous field, so it can be regarded as a Context.

// 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
}

cancelCtx is an unexported type that is exposed to the user by creating the function WithCancel().

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

Pass in a parent Context (usually a background as the root node) and return the new Context. The done channel of the new Context is new.

Note: from the definition of cancelCtx and the generation function WithCancel(), we can see that each generation of cancelCtx based on the parent Context is equivalent to adding a child node to the Context tree of a tree structure. Similar to the following:

Let's first look at the implementation of its Done() method:

func (c *cancelCtx) Done() <-chan struct{} {
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	d = c.done.Load()
	if d == nil {
		d = make(chan struct{})
		c.done.Store(d)
	}
	return d.(chan struct{})
}

c.done is created by lazy initialization. It will be created only when the Done() method is called. Again, the function returns a read-only channel, and there is no way to write data to this channel. Therefore, if you read this channel directly, the process will be block ed. It is usually used with select. Once turned off, the zero value will be read out immediately.

Take another look at the Err() and String() methods. They are relatively simple. Err() is used to return error information and String() is used to return context name.

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

type stringer interface {
	String() string
}

func contextName(c Context) string {
	if s, ok := c.(stringer); ok {
		return s.String()
	}
	return reflectlite.TypeOf(c).String()
}

func (c *cancelCtx) String() string {
	return contextName(c.Context) + ".WithCancel"
}

Let's focus on the implementation of the cancel() method.

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

From the method description, the function of the cancel() method is to close the channel (c.done) to pass the cancellation information, and recursively cancel all its child nodes; If the input parameter removeFromParent is true, it deletes itself from the parent node. The effect is to pass the cancellation signal to all its child nodes by closing the channel. The way goroutine receives the cancellation signal is that the read c.done in the select statement is selected.

When the cancelfunction returned by the WithCancel() function is called or the done channel of the parent node is closed (the cancelfunction of the parent node is called), the done channel of this context (child node) will also be closed.

Pay attention to the parameter passed to the cancel() method. The former is true, that is, when canceling, you need to delete yourself from the parent node. The second parameter is a fixed cancellation error type:

var Canceled = errors.New("context canceled")

It should also be noted that when calling the child node cancel method, the first parameter removeFromParent passed in is false.

When will removeFromParent pass true and when will it pass false?

First, when removeFromParent is true, the current context will be deleted from the parent node.

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}

delete(p.children, child) is to delete itself from the parent node map.

When will it be true? The answer is that when the WithCancel() method is called, that is, when a new context node for cancellation is created, the returned cancelfunction will pass in true. The result of this is: when calling the returned cancelfunction, the context will be "removed" from its parent node, because the parent node may have many child nodes. I have to clear myself and delete myself from the parent node.

In my own cancel() method, all my child nodes will complete the disconnection operation because c.children = nil. Naturally, there is no need to disconnect from me one by one in the cancel() method of all child nodes, and there is no need to do it one by one.


As shown on the left, it represents a context tree. When the cancel method of the red context in the left figure is called, the context is removed from its parent context: the solid arrow becomes a dotted line. In addition, the contexts circled by the dotted line have been cancelled, and the parent-child relationship between the contexts in the circle has disappeared.

In the function WithCancel() that generates cancelCtx, you need to pay attention to one operation, which is propagatecancel (parent, & C).

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		atomic.AddInt32(&goroutines, +1)
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

This function is used to attach the generated current cancelCtx to the "cancelable" parent Context, thus forming the Context tree described above. When the parent Context is cancelled, the cancellation operation can be passed to the child Context.

Here we will focus on explaining why else describes the situation. Else means that if the current node Context does not find a parent node that can be cancelled, it is necessary to start another collaboration to monitor the cancellation action of the parent node or child node.

There is a question here. Since the parent node that can be cancelled is not found, the case < - parent The case of done () will never happen, so you can ignore this case; And case < - child Don't do anything about this case. Isn't this else redundant?

In fact, it's not. Let's look at the code of parentCancelCtx():

// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	done := parent.Done()
	if done == closedchan || done == nil {
		return nil, false
	}
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
	pdone, _ := p.done.Load().(chan struct{})
	if pdone != done {
		return nil, false
	}
	return p, true
}

If the value carried by the parent is not a * cancelCtx, it will be judged as non cancellable. This usually happens when a struct anonymously embeds a Context, which cannot be recognized because of the parent Value (& cancelctxkey) returns * struct instead of * cancelCtx.

3.4.3 timerCtx

timerCtx is a timer Context that can be cancelled. Based on cancelCtx, there is only one more time Timer and a deadline. Timer will automatically cancel the Context when deadline arrives.

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

timerCtx is a cancelCtx first, so it can be cancelled. Take a look at its cancel() method:

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	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()
}

Similarly, timerCtx is also an unexported type, and its corresponding creation function is WithTimeout().

// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// 	func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// 		ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// 		defer cancel()  // releases resources if slowOperation completes before timeout elapses
// 		return slowOperation(ctx)
// 	}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

This function directly calls WithDeadline(). The deadline passed in is the current time plus the timeout time, that is, the timeout time will be counted as timeout from now on. That is, WithDeadline() takes absolute time. Focus on the following:

// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if 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)
	}
	c := &timerCtx{
		cancelCtx: 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, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

In other words, the child node should still be attached to the parent node. Once the parent node is cancelled, the cancellation signal will be transmitted down to the child node, and the child node will be cancelled.

A special case is that if the deadline of the child node to be created is later than that of the parent node, that is, if the parent node is automatically cancelled when the time comes, the child node will be cancelled, resulting in that the deadline of the child node does not work at all, because the child node has been cancelled by the parent node before the deadline arrives.

The core sentence of this function is:

c.timer = time.AfterFunc(d, func() {
    c.cancel(true, DeadlineExceeded)
})

c.timer will automatically call the cancel function after the d time interval, and the error passed in is the timeout error DeadlineExceeded:

// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

3.4.4 valueCtx

valueCtx is a Context only used for value transfer, which carries a key value pair, and other functions are delegated to the embedded Context.

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
	Context
	key, val interface{}
}

Look at the two methods of its implementation.

func (c *valueCtx) String() string {
	return contextName(c.Context) + ".WithValue(type " +
		reflectlite.TypeOf(c.key).String() +
		", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

Because it directly takes Context as an anonymous field, it only implements two methods, and other methods inherit from the parent Context. It is still a feature of Go Context.

Similarly, valueCtx is also an unexported type, and its corresponding creation function is WithValue().

// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) 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}
}

The key is required to be comparable, because the value in the context needs to be retrieved through the key, and comparison is necessary.

By passing the context layer by layer, such a tree is finally formed:

It's a bit like a linked list, but it's in the opposite direction. Context points to its parent node, and the linked list points to the next node. With the WithValue() function, you can create layers of valueCtx to store variables that can be shared between goroutine s.

The process of value taking is actually a recursive search process. Take another look at its Value() method.

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

Because the search direction is upward, the parent node cannot obtain the value stored by the child node, but the child node can obtain the value of the parent node.

The process of WithValue creating Context node is actually the process of creating linked list node. The key values of two nodes can be equal, but they are two different Context nodes. When searching, the last mounted Context node will be found upward, that is, the Context of a parent node close to it. Therefore, on the whole, the list constructed with WithValue is actually an inefficient linked list.

If you have taken over a project, you must have experienced such a dilemma: in a processing process, there are several subfunctions and subprocesses. Various k-v pairs will be inserted into the context in different places, and finally used in a certain place.

You don't even know when, where, and what value? Will these values be "overwritten" (there are two different Context nodes at the bottom, and only one result will be returned when searching)? You're bound to collapse.

This is also the most controversial part of Context. Many people suggest not passing values through Context as much as possible.

4.context usage

4.1 suggestions for use

Generally, we use Background() to obtain an empty Context as the root node. With the root node Context, we can choose to use the following four functions to create the corresponding type of sub Context according to different business scenarios.

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

official context Suggestions on the use of context have been given in the package description document:

1.Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.

2.Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.

3.Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

4.The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

The corresponding Chinese interpretation is:
Don't plug Context into structure 1; Directly take the Context type as the first parameter of the function and name it ctx.

2. Don't pass a nil Context into the function. If you really don't know which Context to pass, please pass it TODO.

3. Do not put the data that should be used as function parameters into the Context and pass it to the function. The Context only stores the data shared between different processes and API s within the request range (such as login information Cookie).

4. The same context may be passed to multiple goroutine s. Don't worry, context is concurrent and safe.

4.2 transferring shared data

For Web server development, we often want to string the whole process of a request processing, which is very dependent on the variable of Thread Local (for Go, it can be understood as unique to a single coprocessor). However, this concept does not exist in Go language, so we need to pass Context when the function is called.

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.Background()
    process(ctx)

    ctx = context.WithValue(ctx, "traceID", "foo")
    process(ctx)
}

func process(ctx context.Context) {
    traceId, ok := ctx.Value("traceID").(string)
    if ok {
        fmt.Printf("process over. trace_id=%s\n", traceId)
    } else {
        fmt.Printf("process over. no trace_id\n")
    }
}

Run output:

process over. no trace_id
process over. trace_id=foo

Of course, in a real scenario, the request ID may be obtained from an HTTP request. Therefore, the following example may be more suitable:

const requestIDKey int = 0

func WithRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(
        func(rw http.ResponseWriter, req *http.Request) {
            // Extract request ID from header
            reqID := req.Header.Get("X-Request-ID")
            // Create valueCtx. Using custom types is not easy to conflict
            ctx := context.WithValue(
                req.Context(), requestIDKey, reqID)

            // Create a new request
            req = req.WithContext(ctx)

            // Call HTTP handler
            next.ServeHTTP(rw, req)
        }
    )
}

// Get request ID
func GetRequestID(ctx context.Context) string {
    ctx.Value(requestIDKey).(string)
}

func Handle(rw http.ResponseWriter, req *http.Request) {
    // Get the reqId and record the log later
    reqID := GetRequestID(req.Context())
    ...
}

func main() {
    handler := WithRequestID(http.HandlerFunc(Handle))
    http.ListenAndServe("/", handler)
}

4.3 cancel goroutine

Context is used to transfer context information between a group of goroutines, and its repetition includes cancellation signal. The cancellation signal can be used to notify the relevant goroutine to terminate the execution to avoid invalid operation.

Let's imagine a scenario: open the order page of takeout, and the location of takeout brother is displayed on the map, which is updated once a second. After the app side initiates a websocket connection request (in reality, it may be polling) to the background, the background starts a collaborative process, calculates the position of the little brother every 1 second, and sends it to the app side. If the user exits this page, the background needs to "Cancel" this process, exit goroutine, and the system reclaims resources.

The possible implementations of the backend are as follows:

func Perform() {
    for {
        calculatePos()
        sendResult()
        time.Sleep(time.Second)
    }
}

If you need to implement the "Cancel" function and do not understand the Context function, you may do this: add a pointer bool variable to the function, judge whether the bool variable changes from true to false at the beginning of the for statement, and exit the loop if it changes.

The simple approach given above can achieve the desired effect. No problem, but not elegant. And once more information is notified, the function input parameters will be very cumbersome and complex. Elegant approach naturally requires Context.

func Perform(ctx context.Context) {
    for {
        calculatePos()
        sendResult()

        select {
        case <-ctx.Done():
            // Cancelled, return directly
            return
        case <-time.After(time.Second):
            // block 1 second 
        }
    }
}

The main process may be as follows:

ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
go Perform(ctx)

// ......
// The app side returns the page and calls the cancel function
cancel()

Pay attention to one detail. The context returned by the WithTimeOut function is separated from the cancelfunction. Context itself has no cancellation function. The reason for this is that the cancellation function can only be called by the outer function to prevent the child node context from calling the cancellation function, so as to strictly control the flow of information: from the parent node context to the child node context.

4.4 prevention of goroutine leakage

In the previous example, goroutine will execute by itself and finally return, but it will waste more system resources. Here is an example where goroutine will leak if you cancel without context (from Using contexts to avoid leaking goroutines).

// gen is an integer generator and will leak goroutine
func gen() <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            ch <- n
            n++
            time.Sleep(time.Second)
        }
    }()
    return ch
}

The above generator will start a goroutine with an infinite loop, and the caller will these values from the channel until n equals 5.

for n := range gen() {
    fmt.Println(n)
    if n == 5 {
        break
    }
}

When n == 5, break directly. Then the co process of the gen function will loop indefinitely and never stop. goroutine leak occurred.

We can use Context to proactively notify gen function's co process to stop execution and prevent leakage.

func gen(ctx context.Context) <-chan int {
	ch := make(chan int)
	go func() {
		var n int
		for {
			select {
			case <-ctx.Done():
				return // Avoid goroutine leakage when ctx ends
			case ch <- n:
				n++
			}
		}
	}()
	return ch
}

Now, the caller can send a signal to the generator when it is finished. After calling the cancel function, the internal goroutine will return.

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // make sure all paths cancel the context to avoid context leak

for n := range gen(ctx) {
    fmt.Println(n)
    if n == 5 {
        cancel()
        break
    }
}

// ...

5. Deficiency of context

Context plays an obvious role. When we are developing background services, it can help us complete the control of a group of related goroutine s and transfer shared data. Note that it is the background service, not all scenarios need to use context.

Go officials suggest that we take context as the first parameter of the function, and even the name is ready. This has a consequence: because we want to control the cancellation of all coroutines, we need to add a context parameter to almost all functions. Soon, in our code, context will spread everywhere like a virus.

In addition, creating functions such as WithCancel, WithDeadline, WithTimeout and WithValue actually create linked list nodes one by one. We know that the operation of linked list is usually O(n) complex and inefficient.

The core problem solved by Context is cancellation. Even if it is not perfect, it simply solves this problem.

6. Summary

Here, the content of the whole context package is finished. The source code is refined and simple, which is very suitable for learning and reading.

In use, first create a context of the root node, and then create a child node context of corresponding functions according to the four functions provided in the context package. Because it is concurrent and safe, it can be delivered safely.

Go 1.7 introduces the context package to solve the cancellation problem of a group of related goroutine s. Of course, it can also be used to transfer some shared data. This kind of scenario is often encountered when developing the background server, so context has its applicable scenario, not all scenarios.

context is not perfect. There are fixed usage scenarios. Do not abuse it.

reference

pkg.go.dev/context
The Go Blog.Go Concurrency Patterns: Context
Using contexts to avoid leaking goroutines
Context should go away for Go 2
Go language design and implementation Context context
Yes Deep decryption of Go language context

Keywords: Go Context

Added by blueguitar on Thu, 03 Feb 2022 21:53:22 +0200