Starting with Go 1.7, the context package was officially introduced into the official standard library. In fact, we often encounter “context” in Go programming, both in general server code and in complex concurrent programs. Today, we’re going to dive into its implementation and best practices.
The official documentation explains the context package as follows.
Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.
In simple terms, “context” can be understood as a running state, a scene, a snapshot of a program unit. Context means that there is an upper and lower layer that passes the content to the lower layer, while the program unit refers to the Goroutine. Each Goroutine needs to know the current execution state of the program before it can be executed, and this execution state is usually encapsulated in a “context” variable that is passed to the Goroutine to be executed. The context package is designed to simplify the handling of multiple Goroutines for a single request and operations related to request deadlines, cancellation signals, and request field data. A common example is that in a Go implementation of a server application, each network request typically requires the creation of a separate Goroutine for processing, which may involve multiple API calls, which in turn may open other Goroutines; since these Goroutines are all processing the same network request, they Since they are all handling the same network request, they often need access to shared resources such as user authentication token rings, request deadlines, etc. And if the request times out or is cancelled, all Goroutines should exit and release the relevant resources immediately. Using contexts allows Go developers to easily implement interactions between these Goroutines, track and control them, and pass request-related data, cancel Goroutine signals or deadlines, etc.
Contextual data structures
The core data structure in the context package is a nested structure or a one-way inheritance structure. Based on the initial context (also called the “root context”), developers can define their own methods and data to inherit from the “root context” depending on the usage scenario; it is this hierarchical organization of the context that allows developers to define some different features in each layer of the context. This hierarchical organization also makes the context easy to extend and its responsibilities clear.
The most fundamental data structure in the context package is the Context
interface.
|
|
The Context
interface defines four methods.
- The
Done()
method returns a read-only channel of any type; using this method the developer can do some cleanup operations after receiving a cancellation request from the context, or when the deadline is up, and then exit the Goroutine and release the relevant resources, or call theErr()
method to get the reason why the context was cancelled, or call theValue()
method to get the the relevant value in the context. - The
Err()
method returns the reason why the context was cancelled; this method is typically called when the channel returned by theDone()
method has data (indicating that the corresponding context was cancelled). Deadline()
method i.e. sets the deadline of the context, at which time the context will automatically initiate a cancellation request; when the second return valueok
isfalse
it means that no deadline has been set and if it needs to be cancelled, the cancellation function needs to be called to cancel it.- The
Value()
method gets the “value” bound to the context by the “key”; this method is thread-safe and, like theErr()
method, is generally called when the channel returned by theDone()
method has data.
Context implementation principles
Since Context is defined as an interface, to use it the developer has to implement the 4 methods defined by the interface. However, this is not the recommended usage of context. The context package defines an implementation of the Context interface called emptyCtx
.
The definition of the emptyCtx
structure we saw above shows that emptyCtx
is an implementation of the Context interface that cannot be cancelled and does not set a deadline or carry any value. However, we do not use it directly, but create two different Context objects based on emptyCtx
by using two factory methods in the context package, and use these two Context objects as the top-level “root context” when we need to start a context, and then These contexts are finally organized into a tree structure; thus, when a context is cancelled, all the contexts inherited from it are automatically cancelled. The factory methods of the two context objects are defined as follows.
- The
Background()
factory method gets the default value of the context and is mainly used in themain()
function, initialization, and test code as the “root context” of the context tree structure, which cannot be cancelled. - The
TODO()
factory method creates a todo context, which is generally used less often and should only be used when you are not sure which context should be used.
Context inheritance derivation
Once you have a “root context”, how do you derive more “child contexts”? This relies on a series of With
functions provided by the context package.
|
|
With these four With
functions, the developer can create a context tree, where each node can have any number of children and any number of node hierarchies.
WithCancel
function, which passes aparent context
as an argument, returns achild context
, and a cancel function to cancel the returned context.WithDeadline
function, similar to theWithCancel
function, but passes an additional deadline parameter, meaning that the corresponding context will be automatically cancelled at that point in time, or, of course, it can be cancelled in advance via the cancel function instead of waiting for that time.- The
WithTimeout
function is basically the same as theWithDeadline
function, except that the parameter passed is the context timeout, meaning that the corresponding context will be automatically cancelled after how much time. WithValue
function is different from the other three functions, it is not used to cancel the context, but only to generate a context with bound key-value pair data, this bound data can be accessed through thecontext.Value
method, generally when you want to pass data through the context you can use this method.
Context best practices
- The “root context” is generally the background context, obtained by calling
context.Background()
. - Do not put the context object in the structure definition, but pass it between functions as a parameter display.
- Generally pass the context as the first argument to every function on the entry request and exit request links, with
ctx
as the recommended variable name. - Do not pass a context with the value
nil
to a function or method, otherwise the context tree will be broken when tracing. - The
Value()
method using a context should pass the required data, don’t pass everything using theValue()
method; context passing data is thread-safe. - a context object can be passed to any number of Goroutines, and when a cancel operation is performed on it, all Goroutines will receive the cancel signal.
The following is my personal summary of typical usage examples of context.
-
Use the
Done()
method to actively cancel the context。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
func process(ctx context.Context, wg *sync.WaitGroup) error { defer wg.Done() respC := make(chan int) // business logic go func() { time.Sleep(time.Second * 2) respC <- 10 }() // wait for signal for { select { case <-ctx.Done(): fmt.Println("cancel") return errors.New("cancel") case r := <-respC: fmt.Println(r) } } } func main() { wg := new(sync.WaitGroup) ctx, cancel := context.WithCancel(context.Background()) wg.Add(1) go process(ctx, wg) time.Sleep(time.Second * 5) // trigger context cancel cancel() // wait for gorountine exit... wg.Wait() }
Output.
-
Timeout automatically cancels the context
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
func process(ctx context.Context, wg *sync.WaitGroup) error { defer wg.Done() for i := 0; i < 1000; i++ { select { case <-time.After(2 * time.Second): fmt.Println("processing... ", i) // receive cancelation signal in this channel case <-ctx.Done(): fmt.Println("Cancel the context ", i) return ctx.Err() } } return nil } func main() { wg := new(sync.WaitGroup) ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() wg.Add(1) go process(ctx, wg) wg.Wait() }
Output.
-
Passing data between Goroutines via the context’s
WithValue()
method1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
func main() { ctx, cancel := context.WithCancel(context.Background()) valueCtx := context.WithValue(ctx, "mykey", "myvalue") go watch(valueCtx) time.Sleep(10 * time.Second) cancel() time.Sleep(5 * time.Second) } func watch(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println(ctx.Value("mykey"), "is cancel") return default: fmt.Println(ctx.Value("mykey"), "int goroutine") time.Sleep(2 * time.Second) } } }
Output.
Summary
The main purpose of “context” in Go is to synchronize cancellation signals or deadlines between multiple Goroutines or modules, to reduce the consumption of resources and long time occupation, and to avoid wasting resources, although passing values is also one of its functions, but this function is rarely used. We should also be very careful not to pass all parameters of a request in “context”, which is a very poor design. The more common usage scenario is to pass the authentication token of the user for the request and the request ID for distributed tracking.
In Go programming, it is usually not possible to kill a Goroutine directly, and the closing of a Goroutine is usually controlled by “channel + select
”. However, in some scenarios, such as when many Goroutines are created to handle a request, and these Goroutines need to share some global variables, have a common deadline, etc., and can be closed at the same time, it is more difficult to use “channel + select
” in this case, but You can easily cope with this by using “context”.