How to use the context.Context package with the Go Language
On this post I will talk about the context package officially released on the 1.7 version of Go, and how to use it correctly. Not totally required but it would be nice if you were at least familiar with it, so if you haven't yet you should read this blog post, from the Golang.org blog that talks about the package and how it is used. Either way, I'll start with what it is.
What is the context.Context package?
In Go servers, each incoming request is handled in its own goroutine. Request handlers often start additional goroutines to access backends such as databases and RPC services. The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user, authorisation tokens, and the request’s deadline. When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.
That is where the context package comes it. With it, it is easy to pass along request scoped values, cancelation signals across a chain of requests and deadlines across API boundaries to all goroutines involved in the request.
What is in it?
The core of the context package is the Context type which is an interface.
// A Context carries a deadline, cancelation signal, and request-scoped values across API boundaries. Its methods are safe for simultaneous use by multiple goroutines.
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{}
}
The Done
method returns a channel that sends a cancelation signal to functions running on behalf of the Context
. When the channel is closed, the functions should abandon their work and return.
The Err
method returns an error indicating why the Context
was canceled.
The Deadline
method allows functions to determine whether they should start work at all; if too little time is left, it may not be worthwhile. Code may also use a deadline to set timeouts for I/O operations.
Value
allows a Context
to carry request-scoped data. That data must be safe for simultaneous use by multiple goroutines.
Adding a value to a context, foo being the key and bar being the value
ctx = context.WithValue(ctx, “foo”, “bar”)
Getting a value from context:
ctx.Value(“foo”)
How to integrate the context package into your API?
One of the most important things to remember when integrating Context into your API is the it is supposed to be request scoped. For example, it would make sense to exist along a single database query, but wouldn’t make sense to exist along a database object.
Here are two ways to integrate Context objects into your API:
- The first parameter of a function call
- Optional config on a request structure
For an example of the first, see package net, Dialer.DialContext. This function does a normal Dial operation, but cancels it according to the Context object.
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
For an example of the second way to integrate Context, see package net/http’s Request.WithContext
func (r *Request) WithContext(ctx context.Context) *Request
This creates a new Request object that ends according to the given Context.
Context should flow through your application
Think of a context like the water in a river. The context should flow through your application and generally not store it like in a struct. Also, you do not want to keep it around anymore that needed. Context should be passed from function to function down your call stack, augmented upon if needed. Ideally, a Context object is created with each request and expires when the request is over.
The one exception to not storing a context is when you need to put it in a struct that is used purely as a message that is passed across a channel. This is shown in the example below.
In this example, we break this general rule of not storing Context by putting it in a message. However, this is an appropriate use of Context because it still flows through the program as a message, but along a channel rather than a stack trace. Also notice here how the Context is used in four places:
- To time out q in case the processor is too full
- To let q know if it should even process message
- To time out q sending the message back to newRequest()
- To time out newRequest() waiting for a response back from ProcessMessage
What should and not go into Context.Value and request-scoped values?
One of the most important parts of the Context type is Value, which allows arbitrary values placed into a Context. The intended use for Context.Value, from the original blog post, is request-scoped values. A request scoped value is one derived from data in the incoming request and goes away when the request is over. As a request bounces between services, this data is often maintained between RPC calls.
Obvious request scoped data could be who is making the request (user ID), some form of request ID (correlation ID), how they are making it (internal or external), from where they are making it (user IP),and how important this request should be.
A database connection is not a request scoped value because it is global for the entire server. On the other hand, if it is a connection that has metadata about the current user to auto populate fields like the user ID or do authentication, then it may be considered request scoped.
A logger is not request scoped if it sits on a server object or is a singleton of the package. However, if it contains metadata about who sent the request and maybe if the request has debug logging enabled, then it becomes request scoped.
As you can see, request scoped data can include a large set of information since in some sense all the interesting data in the application comes from a request. This makes it very difficult to decide what should or not be included in Context.Value, which makes it easy to abuse. But let's try to clarify, through some basic rules I try to follow.
Context.Value obscures the information flow
One of the main reasons so many restrictions are placed on proper use of Context.Value is that it obscures expected input and output of a function or library. This is why it is so hated by so many people. Parameters to a function are clear, you can see it, self sufficient documentation of what is required to make the function behave. This makes the function easy to write tests and reason about intuitively, as well as refactor later. For example, consider the following function that does authentication from Context.
func IsUserRootAndAdmin(ctx context.Context) bool {
x := token.GetToken(ctx)
userObject := auth.AuthenticateToken(x)
return userObject.IsAdmin() || userObject.IsRoot()
}
When users call this function they only see that it takes a Context. But the required parts to knowing if a user is an Admin are clearly two things: an authentication service (in this case creating a dependency from elsewhere) and an authentication token. This makes extremely hard to test and reason about this function.
Let’s clearly represent this flow with a function, but in a more clear way.
func IsUserRootAndAdmin(token string, authService AuthService) int {
userObject := authService.AuthenticateToken(token)
return userObject.IsAdmin() || userObject.IsRoot()
}
This function definition is now much more clear of what is required to know if a user is an admin and root user type. This representation is also apparent to the user of the function and makes testing, refactoring and reusing the function a lot easier.
Context.Value in real life projects
I completely understand the need to add items in Context.Value. The more complex the system, the more middleware layers and multiple abstractions along the call stack, you will have. Values calculated at the top of the call stack are tedious, difficult, and plain ugly to your callers if you have to add them to every function call between top and bottom to just propagate something simple like a user ID, correlation ID or auth token. Imagine if you had to add another parameter called “correlation ID” to a dozen of functions between two calls in two different packages just to let package ASD know about what package ZXC found out? The API would look ugly and to say the least, that code would smell.
Context.Value should inform, not control
This is a concept that sometimes it may be hard to wrap your head around it. But just keep this in mind, Inform, not Control. It should never be required input for documented or expected results. Meaning, if you need the value within a context to make your unit test pass about a specific function, something is definitely wrong.
In other words, if your function can’t behave correctly because of a value that may or may not be inside context.Value, then your API is obscuring required inputs way too much. Even if you forget about documentation, there is also the expected behavior of your application. If the function, for example, is behaving as documented but the way your application uses the function has a practical behavior of needing something in Context to behave correctly, then it moves closer to influencing the control of your program, and this smells.
One example of inform is a correlation ID. That generally these are used in logging or other aggregation systems to group requests together. The actual contents of a correlation ID never change the result of an if statement and if a correlation ID is missing it does nothing to modify the result of a function, maybe you will have a hard time find some information in your logs, but that is about it.
A database connection, for example, is a worst case example of an object to place in a context.Value because it obviously controls the program and is required input for your functions.
How not to use Context.Value
Here I’ll show how to stay inside this kind of abstraction while still not needing to abuse Context.Value. Let’s show some example code that uses HTTP middlewares and Context.Value to propagate a user ID found at the beginning of the middleware.
This is an example of how Context.Value is often used in middleware chains to setup propagating a userID along. The first middleware, addUserID, updates the context. It then calls the next handler in the middleware chain. Later the user id value inside the context is extracted and used. In large applications you could imagine these two functions representing anything else on different places.
Now let's show how we could use the same abstraction but without abusing Context.Value.
Here we can still use the same middleware abstractions and still have only the main function know about the chain of middleware, but use the UserID in a type safe way. The variable chainPartOne is the middleware chain up to when we extract the UserID. That part of the chain can then create the next part of the chain, chainWithAuth, using the UserID directly.
As you can see, we can keep Context to just ending early long running functions. We have also clearly documented that struct UseUserID needs a UserID to behave correctly. This clear separation means that when people later refactor this code or try to reuse UseUserID, they know what to expect. And it is much easier to test it.
If you can not use context.Value, don't!
I understand how easy it is to just add something into context.Value and retrieve it later in some galaxy far far away, but the ease of use now is paid generating much pain when refactoring later, not to mention when try to write tests for it. The Context package can be of great use, but if not used correctly it can cause more harm then good. So be careful.