[Learn Intermediate Go] How does context work?

Sang-gon Lee
Jul 13 · 7 min read

First things first, what is context?

Context — in this context ( ͡❛ ͜ʖ ͡❛) — is a construct that controls deadline in Go. It’s provided by the context package.

Deadline of what, you may ask? It can be used for anything, but its initial intent was for deadlines of operations within a single server request. If you’re not sure what that means, don’t worry! That will be explained in the following sections.

Soft Intro

There are three main elements to a context:

  • Cancellation
  • Deadline/timeout
  • Key-value pairs

Cancellation is the capability that lets you cancel the context manually.

Deadline and timeout can be thought of as “planned” cancellation. Instead of you calling the cancellation though, it’s controlled by machine, based on the time value you give.

Context can also carry its own key-value pairs. Typically it contains the information that’s relevant within a single request. I won’t discuss them here as it’s straightforward and there’s nothing special about them related to context.

In the context package, you will notice there are 6 ways to construct contexts. You can attach the above 3 functionalities to a context using a combination of those six:

  • : cancellation
  • and : deadline/timeout and cancellation
  • : key-value pairs
  • and : none

You might be curious about and . Why do they exist if they serve no purpose?

is typically used as a "root" of other types of context, as the contexts of other types must have a "parent" context. It can also be used when you don't want a deadline but you still need to pass a context variable (e.g. a function expects a context as its argument).

(It’s easier if you understand why it’s named as it is. Imagine you’re passing a context without cancellation on the main thread. The operation can possibly run indefinitely, freezing the main thread — not what you want. A is meant to be used to be passed to "background" processes, which don't need to block other processes.)

and are internally equivalent.

How does deadline/timeout work?

Deadline mechanism is quite simple. The context object holds onto the deadline and when it passes, s the context (I know we haven't covered how works yet. It will be explained in the next section!). If you're curious, it watches the time using time.AfterFunc function.

If the parent context’s deadline exceeds before the child context’s does, child context is cancelled as well.

Converse is not true — cancelling child context does not cancel the parent context.

Timeout is exactly same as deadline — it’s just the deadline is set as the time relative to the present (the time that the context is constructed).

How does cancel work?

Okay, here comes the fun part!

Let’s look at the signature of interface:

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

For now, pay special attention to the function. It returns a channel of empty structs.

If you’re familiar with other languages, you can think of this as a object. That is, it might not have any values right now, but will have one sometime in the future.

This “future” resolves (accurately speaking, the channel gets closed) when the context is cancelled — either by explicit cancel or deadline/timeout. The function will also now return a non-nil error.

That’s it. That’s all the context does when it’s cancelled.

Context does not magically cancel the operation itself when the context is cancelled.

If you are the ultimate callee of the function that takes the context, you should handle the cancellation of the context.

Many of the times, you are simply a caller of the function, from a third party library, that takes the context. You could simply pass the context to it:

Or wrap it with the timeout/deadline that you choose to apply in your layer:

And the library will (hopefully) handle the context on their end.

Here is the visual representation of how a context that has timeout of 2 seconds behaves over time:

Example

Here is an example code (you can see the full code in https://github.com/sanggonlee/learn_intermediate_go/02_context/main.go). It demonstrates how to cancel other operations when one of the operations returns an error, when they are run simultaneously.

All the function does is: take three functions, run them simultaneously, and collect any errors from them.

See the 3 calls of ?

  • The first call will send a error to the channel after 1 second.
  • The second call will send a non- error after 2 seconds.
  • The third call will send a error after 3 seconds.

So without taking the context into account, we would expect the output to be:

Error: err

Because only the second call resolves with an error.

Actual console output:

Error: err
Error: context canceled

Originally, only the second call to the function would return an error. But the third call also failed because as soon as the second call was over, it cancelled the context and propagated to the third call as well.

Notice that in the function, the context cancellation is explicitly handled as an error:

case <-ctx.Done():
errs <- ctx.Err()

Diagram of what’s going on:

More examples

I’ll throw some more examples to help your understanding. You can skip this section if you already understood how context works!

You can find the entire code in https://github.com/sanggonlee/learn_intermediate_go/tree/master/02_context/examples.

Cancellation only

What will the console print for the below code?

Output:

After 1 second: <nil>
After 3 seconds: <nil>
Context done!
After 5 seconds: context canceled

Here, the context doesn’t have any timeout or deadline. Instead we are manually cancelling it after waiting for 4 seconds. We’re checking after 1, 3, and 5 seconds, to see at which stage the context is cancelled. Since the context is cancelled at 4 seconds, the context after 5 seconds now contains the error.

Timeout

What will the console print for the below code?

Output:

After 1 second: <nil>
After 3 seconds: <nil>
Context done!
After 5 seconds: context deadline exceeded

This time we’re using to set the timeout of context to 4 seconds, and never manually cancel it. The effect is same as the last example, but notice that we get a different error: "context deadline exceeded"

Cancel before timeout

What will the console print for the below code?

Output:

After 1 second: <nil>
Context done!
After 3 seconds: context canceled
After 5 seconds: context canceled

This time we’re using both the timeout and manual cancel. Although the timeout is set to 4 seconds, the manual cancel happens after 2 seconds. So the context has “context canceled” error after 3 seconds, not the “deadline exceeded” error.

Notice that the error remains as “context canceled” after 4 seconds (i.e. after deadline). So we learn that contexts don’t change once cancelled.

Other important notes

  • Context is thread-safe (goroutine-safe, to be more exact). This makes the context really useful for controlling cancellation in the concurrent setting, like in the first example code.
  • Context is immutable. None of the APIs in the package allows you to mutate the context object. You can only create a "child" context on top of an existing context.
  • Although useful, the key-value pairs shouldn’t be overused. It doesn’t provide type safety, and is prone to key conflicts because the context can be used across multiple libraries/applications

Closing remark

In my opinion, context can easily be the most common knowledge gap for newcomers to the Go language. It’s not the easiest one to understand, yet used in so many places!

I hope you are now more comfortable with context in Go. Thank you for reading.

What is LIG Series?

Learn Intermediate Go series was designed to help you fill some knowledge gaps just beyond the basic Go tutorials. It aims to contain essence extracted from things that commonly frustrate you, or what you’ve missed while working on the first projects after the basic tutorials. Intended audiences are:

  • you have experience in another language but new to Go, or
  • you just finished a basic tutorial (e.g. Tour of Go), but unsure how to gain further knowledge, or
  • you’ve been writing Go for a few months but want to fill some gaps in your knowledge

Table of contents (they are not in order — you can read in the whatever order you want!):

  1. Go Modules
  2. How does context work?

Nerd For Tech

From Confusion to Clarification

Nerd For Tech

NFT is an Educational Media House. Our mission is to bring the invaluable knowledge and experiences of experts from all over the world to the novice. To know more about us, visit https://www.nerdfortech.org/.

Sang-gon Lee

Written by

Software Engineer. Amateur Short Story Writer (in Korean). GitHub: https://github.com/sanggonlee

Nerd For Tech

NFT is an Educational Media House. Our mission is to bring the invaluable knowledge and experiences of experts from all over the world to the novice. To know more about us, visit https://www.nerdfortech.org/.