What is the Context Package in Golang and What is it For?

• Jason Ladd

The Context package actually has a few different uses. But its main overall job is to do just what it probably sounds like its name is referring to. It’s an object that describes the context that different parts of the application are running in at any given point in time so that its functions can be aware of what’s going on. Essentially contexts accomplish this by:

  • Passing data to functions that is relevant to a transition between different major ‘moving parts’ of an application. This data is sometimes referred to as ‘request scoped’ key-value pairs.
  • Mitigating waste of computing resources caused by functions doing work that will be thrown away. This is accomplished through deadlines, timeouts, and cancellation signals.

The most basic way to instantiate a context is by using the context.TODO() and context.Background() methods. Of course, you can name the resulting context anything you want but the convention is to name them ctx. Once you have contexts, you pass them to functions so that the functions can be kept aware of the ‘contextual situation’ they’re running in. For example, are they running standalone? Or are they running as a result of being kicked off by some other function to do work? Does that work still need to be done at a certain point in time?

context.TODO() and context.Background() do exactly the same thing, which is return a non-nil empty context that you can start adding functionality to or deriving child contexts from. The only difference between the two is that using context.TODO() is kind of a self-documenting way to express that you’re not really sure what the rest of your context chain will end up looking like. It signifies that you might need to add values or cancellations later but are not quite sure yet.

More on Child Contexts

When you create a context using one of the methods like context.WithValue(ctx, “key”, “value”), context.WithCancel(ctx), context.WithTimeout(ctx, timeout) or context.WithDeadline(ctx, deadline), the resulting context is not a reference to the context you started with. The context returned from these functions is a child context. When the parent context is canceled, all the child contexts will get canceled too. Child contexts can also have child contexts. All child contexts down the chain get cancelled when the parent does. The chain of function calls that get kicked off from the main one should propagate the main parent context OR child contexts derived from it.

Passing Values

You can pass a value to a function via a context by using context.WithValue, like this:

ctx := context.Background()
ctx := context.WithValue(ctx, “someKey”, “someValue”)

Now, later you could pass that ctx into a function and inside that function access the value of “someKey” like this:

...
Fmt.Printf(“the value of someKey is: %s”, ctx.Value(“someKey”))
...

Of course, you might be wondering, “what’s the point of this when I could just pass the value directly to the function without using context? Well true, you can but the main point of passing values through context is that contexts are scoped to and representative of API boundaries and different processes. They should only be used when data is being passed across process or API boundaries. Some examples of crossing these ‘boundaries’ would be the flow of your program from the client side of the app to the back end via an HTTP request, or your request handler functions fetching data from a database. But whenever you’re working in your main program flow and are not crossing any process or API boundaries, you should just pass values to functions as parameters normally. Contexts are concurrency safe though, so you can pass them between different goroutines whenever necessary.

Conserving Compute Resources

So like I mentioned above, the other powerful feature of contexts is that they help prevent wasting compute resources by letting functions know if they need to continue doing work or not. There are times where a function might be started up so it can go do some compute intensive work, but while it was working on completing it’s task, the function that kicked it of ended up not needed the data it was retrieving anymore.

For example, when a function is initiated by an http request from some client, it doesn’t make sense to continue processing the request if the client has already disconnected. With a context, we can keep our function aware of the connection from that client so it will stop running when there’s no point in doing so anymore. Maybe you can see how this type of functionality can be essential for a modern media streaming application, like for example TikTok. If a user is swiping through videos and swiping away from some before they finish loading, there’s no point in the server wasting resources in continuing to retrieve data for that video. In this case, the context of that specific request can be canceled and the server can free up its resources to be used for the next video a user wants to view. Another example of this is a user opening a page on an app that requires a lot of database calls. The user might change their mind and close out the tab or app while the database is still working on retrieving data. Why waste resources carrying out all those queries that will be discarded? In that case, you can just cancel the context and the function executing the database query can know to quit so it can focus one something else.

context.Context has a .Done() method that returns a channel that gets closed as soon as the context is done. To signal that a context is done, you can cancel it. To have a cancelable context, you have to call context.WithCancel() on it, which will return a child context that points to the one you passed to it and a method to cancel it, like this:

ctx, cancelCtx := context.WithCancel(ctx)

Then, somewhere later in your code, you can simply cancel that context like this:

cancelCtx()

How Canceling Contexts Works Using Channels

So, now we know the context.Context type has a default method called Done that returns a channel which remains in an open state until the context is done. So to determine whether a context is done or not, you can check to see if this channel gets closed. The channel resulting from Done never gets any messages sent to it, but when any channel gets closed, it will start returning nil. This means, when you check the value of that channel, something (the nil value) is there, which lets you know that the context is done. The way to check for value changes in channels is with a select statement. here’s more info about select statements if you need a better understanding of what they do. Here’s an example of using a select statement to listen for a context becoming done:

ctx := context.Background()

resultsChan := make(chan *WorkerResult)

/// -------------------------------
/// Some code that is processing results and sending them to the resultsChan
/// -------------------------------

for {
    select {
    case <- ctx.Done():
        // the jig is up, you can stop now
        return
    case result := <- resultsChan:
        // keep processing results received from the channel!
    }
}

Because of the nature of select statements, the above for loop will run indefinitely but will block further execution until it sees some action from a channel in one of its cases. The channel from ctx.Done() won’t do anything until it is closed (when it will start returning nil), but the resultsChan channel will trigger the second case as long as messages are being received by it. Because only the case that checks for ctx.Done() has a return statement, the loop will continue until it receives a nil from that channel, which will satisfy that case and execute the return in that block, exiting the loop. However it’s important to note that you’ll probably need to also stop sending values to resultsChan when you’re done with the context because if the select statement can get values from both cases, it’ll just pick one randomly.

Deadlines and Timeouts

Another way of cancelling contexts is via deadlines. Once that deadline has been reached, the context will cancel. You can implement cancels with deadlines like this:

deadline := time.Now().Add(3000 * time.Millisecond)
ctx, cancelCtx := context.WithDeadline(ctx, deadline)

Then, after 3 seconds, this context will be canceled whether or not the work associated with it is finished or not.

cancelCtx()

context.WithDeadline() should be used when you want the context to cancel at a specific time, but if you just want to set a time limit until which it should cancel, for example 1 minute after it was created, you can use context.WithTimeout() like this:

timeout := 60 * 1000 * time.Millisecond
ctx, cancelCtx := context.WithTimeout(ctx, timeout)

The concept of deadlines and timeouts are very similar. One example where a deadline might be best is if you wanted to make sure some operation stops running at the end of the day. Then, you could set the context deadline to 12:00am. This would cancel any context that try to run into the next day. An example of when context with timeout would be useful is when you want to make sure a request doesn’t run longer than 1 minute after it was initiated, or something like that. They both use deadline under the hood though. Timeout is just a convenience wrapper for it.

Even if a context is cancelled from a deadline, you will have to call the manual cancel function from it in order to free up resources it was using. Forgetting to do this could cause a memory leak.

Cancelation Messages and Errors

When a context is cancelled, it will return an error via its ctx.Err() method. For example, if it is cancelled due to a deadline, you will get an error message like context deadline exceeded, or if you explicitly called cancel on it, you would see something like context canceled. However there might be some cases where you want to cancel with a specific error message. In those cases, you can use WithCancelCause, WithDeadlineCause, and WithTimeoutCause, which all return a CancelCauseFunc, which accepts an error as a parameter that it will output when the context is cancelled. Here’s an example of that:

ctx, cancel := context.WithCancelCause(parent)
cancel(someCustomError)
ctx.Err() // returns context.Canceled
context.Cause(ctx) // returns someCustomError

If for some reason you need to create a derived (child) context that you do not want to be canceled when the parent is canceled, you can use context.WithoutCancel. The context you get from that won’t return a Deadline or Err and has a nil Done channel.

Real World Examples

So now that you understand how Context works and when you might need it, let’s look at a real world example of where you might see Context being used. Let’s use a database call.

Say you’re building an HTTP API endpoint that retrieves some items from a database, in your main route handler for fetching the items, you might have something like this:

func itemsFetchHandler(w http.ResponseWriter, r *http.Request) {
    // ...
    // Pass request context to enable cancellation
    user, err := getItems(r.Context(), pool) // http.Request has a context in it by default so you don’t have to create one
    // ...
}

You’ll see that we’re passing a context.Context to our database function which will actually handle sending the query to the db. Then inside that function, you might do something like this:

func getItems(ctx context.Context, pl *pgxpool.Pool) ([]Item, error) {
    rows, err := pl.Query(ctx, "SELECT items.id as ID, name as Name, content as Content, items.created_date as CreatedAt, FROM items ORDER BY items.id DESC")
    if err != nil {
        return []Item{}, err
    }
    defer rows.Close()

    // Collect and return the results
    // ...
}

This function accepts the context you passed in that was propagated from the main HTTP request object (http.Request). By default http.Request will send cancellations message to that context. Since that context is being passed into pgxpool.Pool.Query, this database driver will be able to listen for any cancellation messages. Since pgxpool.Pool.Query is designed by default to listen to these cancellation messages, it will cancel the query if it gets any.

Now you can easily implement a way to save compute resources that would be wasted by http requests being cancelled after they kick off database requests. Of course, if you need more granular control or need to do something custom, you can write your own implementation using channels, etc. However, some libraries like net/http and pgxpool have this type of functionality built in because they are very common use cases.

Check out the official docs for more info on context, but this should give you a good overall view of what they’re for and how to use them.

Binary Search in Go

Binary Search is a fundamental searching algorithm that you can use to find a number in a sorted array. The general idea is, since the array is sorted, you can check the value of the middle element, see if the value you're looking for is less than ...

Working with The Graph Data Structure in Go

Representing Graphs in Code Graphs are just a data structure where a bunch of nodes are connected to each other by imaginary lines called edges. They're really similar to linked lists and trees except pretty much any node can be connected to any o...

Binary Trees In Golang

Binary trees are a fairly simple data structure where you have a node that contains some data, and a max of two child nodes that also contain some data, like this: 1 / \ 2 3 Each child node can also have up to two child nodes, and...