Computer scienceProgramming languagesGolangPackages and modulesStandard libraryContext package

Context package

13 minutes read

In real life, we don't always explain every term or topic during a conversation. This surrounding information is called context, and the concept applies almost similarly in Go.

The context package in Go is used to carry deadlines, cancellation signals, and request-specific values across API boundaries and between processes. Essentially, it's a concurrency pattern that ensures data remains relevant and synchronized throughout different parts of a program.

What is the context?

Context is a convenient way to organize communication between processes. You might think, "Aren't channels already solving the same problem as contexts?"

Well yes, but actually no

Channels are primarily used to send and receive data between goroutines. Data here means a piece of information you need to do something with. Contexts are used to share state or send signals between goroutines. The state is a set of request-scoped values, which is only relevant during a single atomic piece of processing, such as processing an actual web request.

It might be a little bit disorienting to see that context is implemented using channels. Remember that it is just an implementation detail and that such concepts as contexts and channels are not necessarily related.

Here is what the Context interface looks like:

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

From its definition, we can conclude that contexts:

  • Can have deadlines

  • Can be canceled (and possibly not only because of deadlines)

  • Can hold values

Pretty simple, right? Now, let's get to how we create contexts in our application:

How do we create a root context?

Creating a root or Background context is as easy as

ctx := context.Background()

context package documentation for the Background function is pretty straightforward: 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.

context package also provides another way to create context: TODO function, which also returns a non-nil and empty context. The difference is only in semantics.

Nothing actually stops you from using TODO instead of Background and vice versa, but the rule of thumb is:

  • use TODO where you want to do something with the context while it is not provided yet, but that will change in the future

  • use Background where you just need to create context

In Go, it is an accepted practice to use contexts by passing them from function to function, instead of having one global context per application. In practice, the way contexts are implemented enables Golang developers to create a single tree or multiple trees of contexts per application.

For now, let's just take a look at how we can pass context between functions just like any other structure:

import "context"

func acceptAndReturnContext(ctx context.Context) context.Context {
    // as you can see, you can both accept and return a context
    //
    // also, notice the name of the variable 'ctx'
    // it is a convention in Go to give that name to Context variables
    return ctx
}

How can we derive context and pass a value using context?

Contexts can be derived, copied, or inherited. All those three terms mean the same thing: it is possible to create a new context from another context.

The context created from the original context is called a child context, or a copy of the original or parent context. There are multiple ways to derive a context, different for different purposes. For now, we'll focus on passing values and using WithValue functions from the context package.

Here is its signature:

func WithValue(parent Context, key, val any) Context

It accepts a parent context, key, and some value. A value can be of any type. Keys also can be of any type, but unlike values, they shouldn't.

context package documentation clearly states: 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 of keys.

Luckily for us, with one little trick, we can still use strings for keys. Let's have a closer look at deriving a context: first, we create a special type for our context keys, so we would be able to use strings as keys while not breaking context package guidelines:

package main

import (
    "context"
    "fmt"
)

// type aliases allow us to still use strings as keys
type myVeryOwnContextKey string

Now, we'll define a function that will do nothing else than look for a value by key for us, while printing it to the console:

func lookForValue(ctx context.Context, key myVeryOwnContextKey) {
    if val := ctx.Value(key); val != nil {
        fmt.Println("value for", key, "is", val)
        return
    }
    fmt.Println(key, "value is not found")
}

And now in our main function, let's create our first context with values:

// we need to start from something
// in case of contexts it is always a Background context
rootCtx := context.Background()

// creating context key
langKey := myVeryOwnContextKey("language")

// our first context will only have a value under `langKey`
// the context is derived from rootCtx
// and `rootCtx` is the parent of `ctx` context
ctx := context.WithValue(rootCtx, langKey, "Go")

Deriving contexts from derived contexts

Context can be derived from derived contexts. Our second context will also have a version, not only language. It will be inherited from ctx, so ctx will be the parent of ctxWithVersion context. It is also worth mentioning here that child contexts have access to their parent's values, but not the other way around.

// creating another context key
versionKey := myVeryOwnContextKey("version")

ctxWithVersion := context.WithValue(ctx, versionKey, "1.7")

// it is also possible to override values of parent contexts
ctxAnotherLang := context.WithValue(ctxWithVersion, langKey, "Java")

Here's the code for checking it:

fmt.Println("ctx:")        // just looking for the key
lookForValue(ctx, langKey) // and for the key which should not be there
lookForValue(ctx, versionKey)

fmt.Println("\\nctx with version:")  // children have access to their parent's values
lookForValue(ctxWitVersion, langKey) // but also have their own values
lookForValue(ctxWithVersion, versionKey)

fmt.Println("\\nctx with another language:") // let's also make sure that overriding works
lookForValue(ctxAnotherLang, langKey)        // and that non-overriden values persist 
lookForValue(ctxAnotherLang, versionKey)

The output of the program should be the following:

ctx:
value for language is Go
version value is not found

ctx with version:
value for language is Go
value for version is 1.7

ctx with another language:
value for language is Java
value for version is 1.7

And a few key points about context values:

  • children have access to all their parent's values

  • values can be added in the child or derived contexts

  • values can be overridden in derived contexts

Why and when to use context.WithValue?

You might be curious why the default implementation of context only allows setting value in derived context and not have some map-like functionality? Is it possible to pass multiple values using context?

Of course, there are workarounds, there always are. But remember that context passes and shares state and not data. The state in this case means a set of request-scoped values. Data is a set of pieces of information we need to process, transform, or work with in some other way. Its relevance does not disappear when the request is processed.

If at some point you see yourself trying to use context as if it's a map, it might actually be that you need something other than context.

Conclusion

Let's go over the main points of the topic:

  • context is a concurrency pattern

  • context can be passed from the function and returned from the function as any other structure

  • context can be derived, or have a parent context or be a child context which all mean the same thing

  • context can hold values, values can be added to context when deriving with WithValue, one value at a time

  • context have access to their parent's values and can also override them

  • also, a context can have deadlines and be canceled, which also cancels all its child contexts, but more on that later

10 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo