Context

7 min read

Authors
banner

In concurrent programs, it's often necessary to preempt operations because of timeouts, cancellation, or failure of another portion of the system.

The context package makes it easy to pass request-scoped values, cancellation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.

Types

Let's discuss some core types of the context package.

Context

The Context is an interface type that is defined as follows:

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

The Context type has the following methods:

  • Done() <- chan struct{} returns a channel that is closed when the context is canceled or times out. Done may return nil if the context can never be canceled.
  • Deadline() (deadline time.Time, ok bool) returns the time when the context will be canceled or timed out. Deadline returns ok as false when no deadline is set.
  • Err() error returns an error that explains why the Done channel was closed. If Done is not closed yet, it returns nil.
  • Value(key any) any returns the value associated with key or nil if none.

CancelFunc

A CancelFunc tells an operation to abandon its work and it does not wait for the work to stop. If it is called by multiple goroutines simultaneously, after the first call, subsequent calls to a CancelFunc do nothing.

type CancelFunc func()

Usage

Let's discuss functions that are exposed by the context package:

Background

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.

func Background() Context

TODO

Similar to the Background function TODO function also returns a non-nil, empty Context.

However, it should only be used when we are not sure what context to use or if the function has not been updated to receive a context. This means we plan to add context to the function in the future.

func TODO() Context

WithValue

This function takes in a context and returns a derived context where value val is associated with key and flows through the context tree with the context.

This means that once you get a context with value, any context that derives from this gets this value.

It is not recommended to pass in critical parameters using context values, instead, functions should accept those values in the signature making it explicit.

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

Example

Let's take a simple example to see how we can add a key-value pair to the context.

package main

import (
	"context"
	"fmt"
)

func main() {
	processID := "abc-xyz"

	ctx := context.Background()
	ctx = context.WithValue(ctx, "processID", processID)

	ProcessRequest(ctx)
}

func ProcessRequest(ctx context.Context) {
	value := ctx.Value("processID")
	fmt.Printf("Processing ID: %v", value)
}

And if we run this, we'll see the processID being passed via our context.

$ go run main.go
Processing ID: abc-xyz

WithCancel

This function creates a new context from the parent context and derived context and the cancel function. The parent can be a context.Background or a context that was passed into the function.

Canceling this context releases resources associated with it, so the code should call cancel as soon as the operations running in this Context complete.

Passing around the cancel function is not recommended as it may lead to unexpected behavior.

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithDeadline

This function returns a derived context from its parent that gets canceled when the deadline exceeds or the cancel function is called.

For example, we can create a context that will automatically get canceled at a certain time in the future and pass that around in child functions. When that context gets canceled because of the deadline running out, all the functions that got the context gets notified to stop work and return.

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithTimeout

This function is basically just a wrapper around the WithDeadline function with the added timeout.

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

Example

Let's look at an example to solidify our understanding of the context.

In the example below, we have a simple HTTP server that handles a request

package main

import (
	"fmt"
	"net/http"
	"time"
)

func handleRequest(w http.ResponseWriter, req *http.Request) {
	fmt.Println("Handler started")
	context := req.Context()

	select {
	// Simulating some work by the server, waits 5 seconds and then responds.
	case <-time.After(5 * time.Second):
		fmt.Fprintf(w, "Response from the server")

	// Handling request cancellation
	case <-context.Done():
		err := context.Err()
		fmt.Println("Error:", err)
	}

	fmt.Println("Handler complete")
}

func main() {
	http.HandleFunc("/request", handleRequest)

	fmt.Println("Server is running...")
	http.ListenAndServe(":4000", nil)
}

Let's open two terminals. In terminal one we'll run our example.

$ go run main.go
Server is running...
Handler started
Handler complete

In the second terminal, we will simply make a request to our server. And if we wait for 5 seconds, we get a response back.

$ curl localhost:4000/request
Response from the server

Now, let's see what happens if we cancel the request before it completes.

Note: we can use ctrl + c to cancel the request midway.

$ curl localhost:4000/request
^C

And as we can see, we're able to detect the cancellation of the request because of the request context.

$ go run main.go
Server is running...
Handler started
Error: context canceled
Handler complete

I'm sure you can already see how this can be immensely useful.

For example, we can use this to cancel any resource intensive work if it's no longer needed or has exceeded the deadline or a timeout.

I hope this tutorial was helpful, I'll see you in the next one!

© 2022 Shiva Poudel