Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

Introduced in 1.7, the context package provides us a way to deal with context in our application. Those contexts can help when it comes to the cancellation of a task or defining timeout. It can also be useful to propagate request-scoped values through the context, but for this article, we will focus on the cancellation feature.

Default contexts

The Go context package builds a context based on an existing one. Two default contexts exist in the package called TODO and Background:

var (

background = new(emptyCtx)

todo = new(emptyCtx)

)



func Background() Context {

return background

}



func TODO() Context {

return todo

}

As we can see, both of them are empty contexts. This context is a simple context that is never cancelled and does not carry any value.

You can use the background context as a main context, and it will be derived to create new ones. Based on that, you should not use this context in a package directly; it should be used in your main function. If you are building a server with the net/http package, the main context will be provided by the request:

net/http/request.go func (r *Request) Context() context.Context {

if r.ctx != nil {

return r.ctx

}

return context.Background()

}

If you are working inside your own package and do not have any context available, in this case you should use the TODO context. In general, or if there is any doubt about which context you have to use, you can use the TODO context. Now that we know the main contexts, let’s see how to build the derived ones.

Contexts tree

Deriving a context with context creates a link between the derived context and the parent context that is tracked in its internal struct:

type cancelCtx struct {

Context



mu sync.Mutex

done chan struct{}

children map[canceler]struct{}

err error

}

The field children keeps track of all children created from this context, while Context points to the context the current one has been created from.

Here is an example of the creation of some contexts and sub-contexts:

Each context is linked to each other, and if we cancel the main “C” context, all the children will be cancelled as well. Go loops on the children to cancel them one by one:

context/context.go func (c *cancelCtx) cancel(removeFromParent bool, err error) {

[...]

for child := range c.children {

child.cancel(false, err)

}

[...]

}

The cancellation goes down and will never notify the parent about it. If we cancel C1, it will notify only C11 and C12:

This cancellation propagation allows us to define more advanced cases that could help us deal with multiple/heavy jobs depending on a main context.

Cancellation propagation

Let’s take a simple example of a cancellation process via 2 goroutines, A and B, that will work in parallel and cancel the work of the other one if an error occurs thanks to a common context:

If nothing wrong happens, each process will do its job properly. I have added a trace on each job, that will allow us to a see a small tree:

A - 100ms

B - 200ms

-> A1 - 100ms

-> A11 - 50ms

-> B1 - 100ms

-> A12 - 300ms

-> B2 - 100ms

-> B21 - 150ms

Each job is well executed. Now, let’s try to simulate an error in A11:

A - 100ms

-> A1 - 100ms

B - 200ms

-> A11 - error

-> A12 - cancelled

-> B1 - 100ms

-> B2 - cancelled

-> B21 - cancelled

As we can see, A12 is interrupted when B2 and B21 are cancelled to avoid making unnecessary work:

We can see here that context is safe for multiple goroutines. Indeed, it is possible thanks to a mutex we have seen previously in the struct that guarantees a concurrency-safe access to the context.

Case of context leak

As we have seen in the internal struct, the current context keeps a link to its parent in the Context attribute, while the parent keeps the current context in the children attribute. A call to the cancel function will detach the children and parent from the current context:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {

[...]

c.children = nil



if removeFromParent {

removeChild(c.Context, c)

}

}

If the cancel function is not called, the main context will always keep a link to the created context, leading to possible memory leak.

A simple way to detect it is to run go vet command that will warn you about a possible leak:

the cancel function returned by context.WithCancel should be called, not discarded, to avoid a context leak

Conclusion

context package has two other functions that take advantage of the cancel function: WithTimeout and WithDeadline . Both of them will automatically trigger the cancel function after the defined timeout/deadline.