When using some of Golang’s frameworks, such as Gin, the Handler method of each request always needs to pass in a context object, and then a lot of request data, such as request parameters, path variables, etc. can be read out from it, in fact, in the process of using this has generally understood what the context is, but for some of the details including the specific use of the lack of understanding, so this article on the golang inside the concept of context for a brief discussion.

A few ways to control Goroutine concurrency

We know that Golang is a highly concurrent programming language that can quickly create concurrent tasks with Goroutines, but how to effectively manage these executing Goroutines is a question to think about. Usually, we have the following ways to implement Goroutine control.

  • Using WaitGroup, the root goroutine keeps track of the number of opened goroutines via add() and waits for done() of the goroutine executing the task via wait(), enabling synchronous work.
  • using for/select + stop channel to achieve the end of a goroutine by passing a stop signal to the stop channel.
  • using Context, which allows controlling goroutine tasks with complex hierarchical relationships, when using the first two ways of implementation would be more complex and using context would be more elegant.

Overview of the Context Principle

Goroutine creation and invocation relationships are often hierarchical, and the top Goroutine should have a way to actively shut down the execution of its subordinate Goroutines. To achieve this relationship, the Context structure should also be like a tree, where the leaf nodes must always be derived from the root node.

The first goroutine that creates a Context is called the root node: the root node is responsible for creating a concrete object that implements the Context interface and passing that object as an argument to the newly pulled up goroutine as its context. The downstream goroutine continues to encapsulate the object and so on down the line.

Contexts can be safely used by multiple goroutines. The developer can pass a Context to any number of goroutines and then notify all of them when the context is canceled.

Context interface source code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}
  • Done: Returns a closed channel when the Context is cancelled or times out. The closed channel can be used as a broadcast notification to tell the context-related function to stop its current work and return.
  • Err : Returns why the context was cancelled.
  • Deadline: Returns when the context will time out.
  • Value : The data associated with context

Create the root Context

The top Goroutine should have a way to actively shut down the execution of its subordinate Goroutines (otherwise the program might get out of control). To achieve this relationship, the Context structure should also be like a tree, where the leaf nodes must always be derived from the root node.

There are two ways to create the root Context.

  1. context.Background()
  2. context.TODO()

context.Background()

The first way to create a root context.

By calling context.Background() in the top-level goroutine, you can return an empty Context, which is the root of all Contexts and cannot be cancelled.

context.TODO()

The second way to create the root context.

Typically the root context is created using the Background() method. TODO() is used when you are currently unsure what context to use and leave it for later adjustment.

Note: Do not pass a nil context, you should use context.TODO() when you are not sure what context to use.

Sub-Context Derived Methods

If the parent context is canceled, then all its derived contexts receive the cancel signal, i.e. the channel returned by Done() reads the data.

There are four ways to derive a context.

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
  2. func WithValue(parent Context, key, val interface{}) Context
  3. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
  4. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

context.WithCancel()

The most common type of context derivation takes a parent context (either a background context, or another Context) and returns a derived context and a cancel function object for control.

WithCancel returns an inherited context that context closes its own Done channel when the parent context’s Done is closed, or closes its own Done when it is Canceled. (Note: Read-closed channels return a type zero value)

WithCancel also returns a cancel function, cancel, which is used to cancel the current Context. When the parent task executes cancel(), all goroutines of the receiving context will read the value from the channel returned by Done() and exit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func job() {
    ctx, cancel := context.WithCancel(context.Background())
    go doSomething(ctx)

    time.Sleep(5 * time.Second)
    cancel()
}

func doSomething(ctx context.Context) {
    for {
        time.Sleep(1 * time.Second)
        select {
        case <-ctx.Done():
            fmt.Println("done")
            return
        default:
            fmt.Println("working")
        }
    }
}

context.WithValue()

Derive a context that carries information for passing.

For example, carrying authentication information in a Request, carrying user data, etc.

The parameters of the WithValue(parent Context, key, val interface{}) method contain three parts.

  • parent, the parent context used to derive the child context.
  • key, the key carrying the information, type interface{}.
  • value, the value of the message carried, type interface{}, which is usually used after receiving the message by asserting(.(T)) that the value is converted to the correct type.

The information carried by the receiving context can be received as value (of type interface{}) using ctx.Value(K)

context.WithTimeout()

Derives a context with a timeout mechanism.

When the Timeout timeout is reached, the context and the context’s sub-contexts receive a cancel signal to exit.

Of course, if cancel is called within the Timeout time, the cancel signal will be sent earlier.

1
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)

context.WithDeadline()

derives a context with an absolute deadline, which is basically the same as WithTimeout(), except that the time is set differently.

When the time set by the deadline is reached, the context and the sub-contexts of the context will receive a cancel signal to exit.

Of course, if cancel is called before the deadline, the cancel signal will be sent earlier.

1
ctx, cancel := context.WithDeadline(parentCtx, time.Now().Add(5*time.Second))

Passing and control between hierarchical Contexts

  • Lifecycle: The lifecycle of a Context object is generally only one request processing cycle. That is, a Context variable is created for a request (it is the root of the Context tree); after the request is processed, this ctx variable is revoked and the resources are released.
  • Passing method: Each time a Goroutine is created, either the original Context is passed to the Goroutine, or a child Context is created and passed to the Goroutine.
  • Secure Read/Write: The Context has the flexibility to store different types and numbers of values, and to enable multiple Goroutines to read and write the values in it securely.
  • Control: When creating a child Context object from a parent Context object, you can simultaneously obtain a Cancel function object for the child Context, thus gaining control over the child task.

Principles of use

  • When passing a Context, it should not be passed into the struct, but should be passed explicitly into the function and placed first in the argument list, usually named ctx.
  • Do not pass a nil Context, and pass context.TODO() if you are not sure.
  • Value-related methods using context should be used only to pass request-related metadata, not to pass some optional parameters with it.
  • the same context can be passed to different goroutines and be safely accessible in multiple goroutines.