The context
1 package was introduced in Go 1.7 to provide a cleaner way than using channels to manage cancellation and timeouts across goroutines.
While the scope and API footprint of the package is pretty small, it was a welcome addition to the language when introduced.
The context
package in Listing 12.1 defines the context.Context
2 type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
2. https://pkg.go.dev/context#Context
Listing 12.1 The context
Package
Context is, mostly, used for controlling concurrent subsystems in your application. In this chapter, we cover the different kinds of behavior with contexts, including canceling, timeouts, and values. We also explain how you can clean up a lot of code involving channels by using contexts.
The context.Context
interface, Listing 12.2, consists of four methods. These methods provide the ability to listen for cancellation and timeout events and retrieve values from the context hierarchy. They also provide a way to check what error
, if any, caused the context to be canceled.
Listing 12.2 The context.Context
Interface
In Listing 12.2, you can see that the context.Context
interface implements several of the channel patterns we talk about in Chapter 11, such as having a Done
channel that can be listened to for cancellation.
We cover each of these methods in more detail later. For now, let’s briefly look at each one of them.
Context#Deadline
You can use the context.Context.Deadline
3 method, Listing 12.3, to check whether a context has a cancellation deadline set and, if so, what that deadline is.
3. https://pkg.go.dev/context#Context.Deadline
Listing 12.3 The context.Context.Deadline
Method
Context#Done
You can use the context.Context.Done
4 method, Listing 12.4, to listen for cancellation events. This is similar to how you can listen for a channel being closed, but it is more flexible.
4. https://pkg.go.dev/context#Context.Done
Listing 12.4 The context.Context.Done
Method
Context#Err
You can use the context.Context.Err
5 method, Listing 12.5, to check whether a context has been canceled.
5. https://pkg.go.dev/context#Context.Err
Listing 12.5 The context.Context.Err
Method
Context#Value
You can use the context.Context.Value
6 method, Listing 12.6, to retrieve values from the context hierarchy.
6. https://pkg.go.dev/context#Context.Value
Listing 12.6 The context.Context.Value
Method
The context
package, Listing 12.7, provides a number of useful helper functions for wrapping a context.Context
, making the need for custom implementations of the context.Context
interface less common.
Listing 12.7 The context
Package
Although often you might be given a context.Context
, you might also be the one to start a context.Context
. The most common way to provide a quick and easy way to start a context.Context
is to use the context.Background
7 function, Listing 12.8.
7. https://pkg.go.dev/context#Background
Listing 12.8 The context.Background
Function
In Listing 12.9, we print the context.Context
returned by context.Background
. As you can see from the output, the context is empty.
Listing 12.9 The context.Background
Function
Although the context.Background
interface is empty, it does provide default implementations of the context.Context
interface, Listing 12.10. Because of this, the context.Background
context is almost always used as the base of a new context.Context
hierarchy.
Listing 12.10 The context.Background
Function Provides Default Implementation of the context.Context
Interface
According to the context
documentation, the following rules must be adhered to when using the context
package:
Programs that use contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation.
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.
Do not pass a nil
context, even if a function permits it. Pass context.TODO
if you are unsure about which context to use.
Use context values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
The same context may be passed to functions running in different goroutines; contexts are safe for simultaneous use by multiple goroutines.
As the context
documentation states, a context.Context
is not meant to be stored and held onto but should be passed at runtime.
Consider an HTTP request. An HTTP request is a runtime value that gets passed along through the application until eventually the response is returned. You would not want to store, or hold on to, the request for future use because it would be of no benefit once the response is returned.
Using context.Context
in your code behaves like the HTTP request. You pass a context.Context
through the application where it can be listened to for cancellation, or for other purposes, at runtime.
As a context.Context
is passed through the application, a receiving method may wrap the context with its cancellation functionality or with context.WithValue
to add a value, such as a request ID, before passing the context.Context
along to any functions or methods that it may call.
The result is a nodal hierarchy of context.Context
values that starts at the beginning of the request or the start of the application and spiders out throughout the application.
Consider Listing 12.11. We start with a context.Background
context and pass it the A
and B
functions. Each function wraps the given context.Context
, prints that new context.Context
with a new one, and then either passes it along to the next function or returns.
Listing 12.11 Wrapping Context Creates Nodal Hierarchies
To wrap the context.Context
with a new one, we use context.WithValue
. The context.WithValue
function takes a context.Context
and a key and value, and it returns a new context.Context
with the given key and value that wraps the original context.Context
. We discuss more about context.WithValue
later in this chapter.
In Listing 12.12, we define the functions used in Listing 12.11. Each of these functions takes a context.Context
as an argument. They then wrap the context.Context
with a new one, print the new context.Context
with a new one, and pass it along to the next function.
Listing 12.12 The Example Application
When you look at the output of the program, Listing 12.13, you can see that when we print out any given context.Context
, we see that it is at the bottom of the node tree, and the context.Background
context is at the top of the node tree hierarchy.
Listing 12.13 Printing the Node Tree
In Figure 12.1, you see that the B1a context.Context
is a child of the B1 context.Context
, the B1 context.Context
is a child of the B context.Context
, and the B context.Context
is a child of the original background context.Context
.
As we have seen, one feature of the context
package is that it allows you to pass request specific values to the next function in the chain.
This provide a lot of useful benefits, such as passing request or session specific values, such as the request id, user id of the requestor, etc. to the next function in the chain.
Using values, however, has its disadvantages, as we will see shortly.
You can use the context.WithValue
8 function to wrap a given context.Context
with a new context.Context
that contains the given key/value pair (see Listing 12.14).
8. https://pkg.go.dev/context#WithValue
Listing 12.14 The context.WithValue
Function
The context.WithValue
function takes a context.Context
as its first argument and a key and a value as its second and third arguments.
Both the key and value are any
values. Although this may seem like you can use any type for the key, this is not the case. Like maps, keys must be comparable, so complex types like maps or functions are not allowed. In Listing 12.15, for example, when trying to use a map as a key for the context, the application crashes with a panic.
Listing 12.15 A Compilation Panic because of a Noncomparable Key Type in a Map
When you ask for a key through the context.Context.Value
function, the context.Context
first checks whether the key is present in the current context.Context
. If the key is present, the value is returned. If the key is not present, the context.Context
then checks whether the key is present in the parent context.Context
. If the key is present, the value is returned. If the key is not present, the context.Context
then checks whether the key is present in the context.Context
’s parent’s parent, and so on.
Consider the example in Listing 12.16. We wrap a context.Context
multiple times with different key/values.
Listing 12.16 Nesting and Printing Different Contextual Nodes
From the output in Listing 12.17, you can see that the final context.Context
has a parentage that includes all of the values added with context.WithValue
. You can also see that we are able to find all of the keys, including the very first one that we set.
Listing 12.17 Output Demonstrating Context Nodal Ancestry
As is mentioned in the context
documentation, shown in Listing 12.18, using string keys is not recommended. As you just saw when context.Context.Value
tries to resolve a key, it finds the first, if any, context.Context
that contains the key and returns that value.
Listing 12.18 Using Strings as Context Keys Is Not Recommended per the Documentation
When you use the context.Context.Value
function, you get the last value that was set for the given key. Each time you use context.WithValue
to wrap a context.Context
with a new context.Context
, the new context.Context
essentially will have replaced the previous value for the given key. For example, in Figure 12.2, Context B
is setting the key request_id
with the value B-123
. Context B1
also sets a value, B1-abc
, using the request_id
key. Now, any child nodes of Context B1
, such as Context B1a
, will have a different request_id
from other direct descendants of Context B1
.
Consider the example in Listing 12.19. We wrap a context.Context
multiple times, each time with a different value but the same key, request_id
, which is of type string
.
Listing 12.19 Overwriting Context Values by Using Identical Keys
When we try to log the request_id
for both A
and B
, we see that they are both set to the same value (as in Listing 12.20).
Listing 12.20 The Value Set in Function A
Is Overridden by Function B
One way to solve this problem is to try to “namespace” your string
keys, myapp.request_id
. Although you may never get into a collision scenario, the possibility of someone else using the same key does exist.
Because Go is a typed language, you can leverage the type system to solve the problem of key collisions. You can create a new type based on string
that you can use as the key, as in Listing 12.21.
Listing 12.21 Using Different Types for Keys Can Prevent Collisions
The Logger
is now properly able to retrieve the two different request_id
values because they are no longer of the same type (see Listing 12.22).
Listing 12.22 The Value Set in Function A
Is No Longer Overridden by Function B
This code can be further cleaned up by using constants for the keys that our package, or application, uses. This allows for cleaner code and makes it easier to document the potential keys that may be in a context.Context
. For example, in Listing 12.23, new constants, such as A_RequestID
, are defined and exported by the package along with documentation for the constants’ usage. These constants can now be safely used to set and retrieve context values.
Listing 12.23 Using Constants Can Make Working with Context Values Easier
If we export or make public the types and names of the context.Context
keys our package or application uses, we run the risk of a malicious agent stealing or modifying our values. For example, in a web request, we might set a request_id
at the beginning of the request, but a piece of middleware later in the chain might modify that value to something else. In Listing 12.24, the WithBar
function is replacing the value set in the WithFoo
function by using the exported foo.RequestID
key.
Listing 12.24 Exporting Context Keys Can Lead to Malicious Use
The best way to ensure that your key/value pairs aren’t maliciously overwritten or accessed is by not exporting the types, and any constants, used for keys. In Listing 12.25, the constant requestID
is not exported and can only be used within its defining package.
Listing 12.25 Unexported Context Keys with Custom Types Provide the Best Security
Now you are in control of what values from the context you want to make public. For example, you can add a helper function to allow others to get access to the request_id
value.
Because the return value from context.Context.Value
is an empty interface, interface{}
, you can use these helper functions to not just retrieve access to the value but also type assert the value to the type you want or return an error if it doesn’t. In Listing 12.26, the RequestIDFrom
function takes a given context.Context
and tries to extract a value using the nonexported custom type context key. In addition to hiding details about the key, it also hides details of how the value is stored. In the future, if the value is no longer stored as a string
but rather as a struct
, this function prevents external API breakage.
Listing 12.26 Future-Proofing Implementation Details with a Helper Function
Our application can be updated to use the new helper function to print the request_id
or exit if there was a problem getting the value (see Listing 12.27).
Listing 12.27 Using the RequestIDFrom
Function to Properly Retrieve a Context Value
The malicious bar
package can no longer set or retrieve the request_id
value set by the foo
package (Listing 12.28). The bar
package does not have the ability to create a new type of value foo.ctxKey
because the type is unexported and cannot be accessed outside of the foo
package.
Listing 12.28 The WithBar
Function Can No Longer Maliciously Access Values Set by WithFoo
As a result of securing our context.Context
values, the application now correctly retrieves the request_id
value set by the foo
package (see Listing 12.29).
Listing 12.29 The Request ID Set by WithFoo
Is Now Safe-Guarded
Although having the ability to pass contextual information via a context.Context
is useful, the real benefit, and design of the context
package, is that it can be used to propagate cancellation events to those listening to the context. When a parent context.Context
is canceled, all its children are also canceled.
To cancel a context.Context
, you must have a way of cancelling it. The context.WithCancel
function, Listing 12.30, wraps a given context.Context
with a context.Context
that can be canceled.
Listing 12.30 The context.WithCancel
Function
The context.WithCancel
9 function returns a second argument—that of a context.CancelFunc
10 function, which can be used to cancel the context.Context
.
9. https://pkg.go.dev/context#WithCancel
10. https://pkg.go.dev/context#CancelFunc
There a few things that need to be noted about the context.CancelFunc
function, Listing 12.31. So let’s examine each in more detail.
Listing 12.31 The context.CancelFunc
Function
“After the first call, subsequent calls to a CancelFunc do nothing.”
—context.CancelFunc
documentation
According to the context.CancelFunc
documentation, the context.CancelFunc
function is idempotent, Listing 12.32. That is, calling it multiple times has no effect beyond the first call.
Listing 12.32 The Idempotent Behavior of the context.CancelFunc
Function
“Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.”
—context.WithCancel
documentation
Often you will want to defer execution of the context.CancelFunc
function, as in Listing 12.33, until the function or application exits. This ensures proper shutdown of the context.Context
and prevents the context.Context
from leaking resources.
Listing 12.33 Prevent Leaking Goroutines by Calling the context.CancelFunc
Consider Listing 12.34. The listener
function takes a context.Context
as its first argument and an int
representing the goroutine id as its second argument.
The listener
function blocks until the context.Context
is canceled, which closes the channel behind context.Context.Done
method. This unblocks the listener
function and allows it to exit.
Listing 12.34 Blocking on context.Context.Done
The application creates a context.Background
context and then wraps it with a cancellable context.Context
. The context.CancelFunc
returned is immediately deferred to ensure the application doesn’t leak any resources.
In Listing 12.35, we create several goroutines that listen for the context.Context
to be canceled.
Listing 12.35 Using Context Cancellation
As you can see from the output in Listing 12.36, the listener
function unblocks and exits when the context.CancelFunc
is called, cancel()
.
Listing 12.36 The Output of the Application
Figure 12.3 shows that by canceling a node in the hierarchy, all its child nodes are also canceled. Other notes in the hierarchy, such as parent and sibling nodes, are unaffected.
Previously, we have used time.Sleep
11 to block the execution of the program. This is not a good practice because it can lead to deadlocks and other problems. Instead, the application should receive a context.Context
cancellation confirmation.
11. https://pkg.go.dev/time#Sleep
Consider Listing 12.37. To start a Monitor
, we must use the Start
method, giving it a context.Context
. In return, the Start
method returns a context.Context
that can be listened to by the application to confirm the shutdown of the Monitor
later on.
Listing 12.37 Accepting a context.Context
and Returning a New One
To prevent the application from blocking, we launch the listen
method in a goroutine with the given context.Context
. Unless this context.Context
is canceled, the listen
method never stops and continues to leak resources until the application exits.
The context.CancelFunc
is held onto by the Manager
so when the Manager
is told to cancel by the client, it also cancels the Monitor
context. This tells the client that the Monitor
has been shut down, confirming the cancellation of the Monitor
.
The listen
method blocks until the context.Context
, given by the application, is canceled, Listing 12.38. We first make sure to defer the context.CancelFunc
in the Monitor
to ensure that if the listen
method exits for any reason, clients will be notified that the Monitor
has been shut down.
Listing 12.38 The Monitor
Calls Its context.CancelFunc
if External context.Context
Is Canceled
In Listing 12.39, the application starts with a context.Background
context and then wraps that with a cancellable context.Context
. The context.CancelFunc
returned is immediately deferred to ensure the application doesn’t leak any resources. After a short while, in a goroutine, the cancel
function is called, and the context.Context
is canceled.
The Monitor
is then started with our cancellable context.Context
. The context.Context
returned by the Start
method is listened to by the application. When the Monitor
is canceled, the application is unblocked and can exit. Alternatively, if the application is still running after a couple of seconds, the application is forcibly terminated.
Listing 12.39 Using the Cancellation Confirmation
As you can see from the output in Listing 12.40, the application waits for the Monitor
to properly shut down before exiting. We were also able to remove the use of time.Sleep
to allow the monitor to finish.
Listing 12.40 The Output of the Application
In addition to enabling you to manually cancel a context.Context
, the context
package also provides mechanisms for creating a context.Context
that self-cancels after or at a given time. Using these mechanics allows you to control how long to run some before you give up and assume that the operation has failed.
The context
package provides two functions for creating a time-based self-canceling context.Context
: context.WithTimeout
12 and context.WithDeadline
.13
12. https://pkg.go.dev/context#WithTimeout
13. https://pkg.go.dev/context#WithDeadline
When using context.WithDeadline
, Listing 12.41, we need to provide an absolute time at which the context.Context
should be canceled. That means we need an exact date/time we want this context.Context
to be canceled—for example, March 14, 2029 3:45pm
.
Listing 12.41 The context.WithDeadline
Function
Consider Listing 12.42. In it, we create a new time.Time
14 for January 1, 2030 00:00:00
and use it to create a context.Context
that self-cancels at that date and time.
14. https://pkg.go.dev/time#Time
Listing 12.42 Using context.WithDeadline
Although being able to cancel a context.Context
at a particular time is useful, more often than not you want to cancel a context.Context
after a certain amount of time has passed.
When using context.WithTimeout
, Listing 12.43, we need to provide a relative time.Duration
15 at which the context.Context
should be canceled.
15. https://pkg.go.dev/time#Duration
Listing 12.43 The context.WithTimeout
Function
Consider Listing 12.44. In it, we create a new self-canceling context.Context
that uses context.WithTimeout
to self-cancel after 10 milliseconds.
Listing 12.44 Using context.WithTimeout
Functionally, we could have used context.WithDeadline
instead, but context.WithTimeout
is more convenient when we want to cancel a context.Context
after a certain amount of time has passed.
In a complex system, or even in a small one, when a context.Context
is canceled, you need a way to know what caused the cancellation. It is possible that context.Context
was canceled by a context.CancelFunc
successfully, was canceled because it timed out, or was canceled for some other reason.
The context.Context.Err
method in Listing 12.45 returns the error that caused the context to be canceled.
Listing 12.45 The context.Context.Err
Method
The context
package defines two different error
variables that can be used to check an error
that was returned from context.Context.Err
method.
The first is context.Canceled
, Listing 12.46, which is returned when the context is canceled through the use of a context.CancelFunc
function. This error
is considered to indicate a “successful” cancellation.
Listing 12.46 The context.Canceled
Error
Consider Listing 12.47. When we first check the context.Context.Err
method, it returns nil
. After we call the context.CancelFunc
function provided by context.WithCancel
, the context.Context.Err
method returns a context.Canceled
error.
Listing 12.47 Checking for Cancellation Errors
As you can see from the output in Listing 12.47, repeated calls to the context.Context.Err
method return the same context.Canceled
error.
When a context.Context
is canceled due to a deadline or timeout being exceeded, the context.Context.Err
method returns a context.DeadlineExceeded
16 error, as in Listing 12.48.
16. https://pkg.go.dev/context#DeadlineExceeded
Listing 12.48 The context.DeadlineExceeded
Error
Consider Listing 12.49. We create a context.Context
that self-cancels after 1 second. When we check the context.Context.Err
method before the context.Context
times out, it returns nil
.
Listing 12.49 Checking for Deadline Exceeded Errors
As you can see from the output, the context.Context
times out after the specified time, and the context.Context.Err
method returns a context.DeadlineExceeded
error.
Previously, when we discussed channels in Chapter 11, we showed you how to capture system signals, such as ctrl-c
, using signal.Notify
.17 The signal.NotifyContext
18 function, Listing 12.50, is a variant of signal.Notify
that takes a context.Context
as an argument. In return, we are given a context.Context
that will be canceled when the signal is received.
17. https://pkg.go.dev/os/signal#Notify
18. https://pkg.go.dev/os/signal#NotifyContext
Listing 12.50 The signal.NotifyContext
Function
Consider Listing 12.51. We use signal.NotifyContext
to listen for ctrl-c
. This function returns a wrapped context.Context
that will cancel when the signal is received. It also returns a context.CancelFunc
that can be used to cancel context.Context
when needed.
Listing 12.51 Listening for System Signals
Testing system signals is tricky, and you must take care not to accidentally exit your running tests. Unfortunately, the syscall
19 package does not provide a “test” signal or a way to implement a test signal.
19. https://pkg.go.dev/syscall
You can use syscall.SIGUSR1
20 or syscall.SIGUSR2
21 in your tests because these are allocated to the developer to use for their own purposes.
20. https://pkg.go.dev/syscall#SIGUSR1
21. https://pkg.go.dev/syscall#SIGUSR2
When you are testing signals, you are testing a global signal that will be caught by anyone else who is listening to that signal. We want to make that when testing signals, we aren’t running the tests in parallel and that you don’t have other tests also listening to the same signal.
Consider Listing 12.52. How do we test that the Listener
function will respond properly to a signal? We don’t want to make that the responsibility of the Listener
function; it already has a context.Context
that it can listen to for cancellation. The Listener
function doesn’t care why it was told to stop listening; it just needs to stop listening. This could be because we receive an interrupt signal because a deadline has passed or because the application no longer needs the Listener
function to keep running.
Listing 12.52 The Listener
Function
In Listing 12.53, before we call the Listener
function, we first create a context.Context
that self-cancels after 5 seconds if nothing else happens. We then wrap that context.Context
with one received from the signal.NotifyContext
function that self-cancels when the system receives a TEST_SIGNAL
signal.
Our test blocks with a select
waiting for either context.Context
to be canceled and then respond accordingly.
Listing 12.53 Testing the Listener
Function
Inside the test, in a goroutine, we can trigger the TEST_SIGNAL
signal by sending it to the current process, syscall.Getpid
,22 with the syscall.Kill
23 function, as shown in Listing 12.54. We can also see in Listing 12.54 the test successfully exits after 1s.
22. https://pkg.go.dev/syscall#Getpid
23. https://pkg.go.dev/syscall#Kill
Listing 12.54 Sending a TEST_SIGNAL
Signal
In this chapter, we explored the concept of contexts in Go. We explained that contexts are a way to manage cancellation, timeouts, and other request-scoped values across API boundaries and between processes. We also discussed how to use contexts to clean up a lot of code involving channels, such as listening for system signals. We discussed the nodal hierarchy of how the context
package wraps a new context.Context
around a parent context.Context
. We explained the difference was to cancel a context.Context
and how to use multiple context.Context
s to confirm shutdown behavior. The context
package, while small, is a very powerful tool for managing concurrency in your application.