Go is a simple language, but I was pretty confused by the context package when I first learned about it. I had trouble understanding how it could be used to set a deadline or to cancel functions or how it even worked. Since then, I’ve gotten a firmer grasp on how to use it both as a user and as a library author.

Let’s start by defining what a Context is and its purpose. A Context is a construct used to signal a function to stop its work and return. The key is that a function needs to be context aware in order for this to have any affect; there’s no magic.

Here is an example of a context aware function. Notice that we check ctx.Done() to see if the context has been canceled in the function doing the work.

import (
	"context"
	"fmt"
	"time"
)

func doWork(ctx context.Context) (int, error) {
	select {
	case <-time.After(2 * time.Second):
		return 1337, nil
	case <-ctx.Done():
		return 0, ctx.Err()
	}
}

func main() {
	ctx := context.Background()
	fmt.Println(doWork(ctx))

	ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
	defer cancel()
	fmt.Println(doWork(ctx))
}
1337 <nil>
0 context deadline exceeded

In this example, we use a select statement to return immediately when the context is finished, which is generally good practice. By doing so, we respect the callers’ wish to cancel and return early.

Ultimately, contexts are a pretty simple and powerful concept, but requires cooperation with child functions to work well. That is the part that previously confused me. I had incorrectly believed contexts had some magic that would ensure a deadline wasn’t exceeded, but as with most things in Go, there is no magic and the idea is quite simple.