How to correctly use context.Context in Go 1.7

This post will talk about a new library in Go 1.7, the context library, and when or how to correctly use it. Required reading to start is the introductory post that talks a bit about the library and generally how it is used. You can read documentation for the context library on tip.golang.org.

How to integrate Context into your API

There are currently 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’s 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 program

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, 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

All blocking/long operations should be cancelable

In the example above, ProcessMessage is a quick operation that doesn’t block so the context is obviously overkill. However, if it was a much longer operation then the use of Context by the caller allows newRequest to move on if it takes too long to calculate.

Context.Value and request-scoped values (a warning)

Obvious request scoped data could be who is making the request (user 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.

Unfortunately, request scoped data can encompass a large set of information since in some sense all the interesting data in the application comes from a request. This puts a broad definition on what could be included in Context.Value, which makes it easy to abuse. I personally have a more narrow view of what is appropriate in Context.Value and I’ll try to explain my position in the rest of this post.

Context.Value obscures your program’s flow

func IsAdminUser(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 used as a singleton) and an authentication token. You can represent this as inputs and outputs like below.

IsAdminUser flow

Let’s clearly represent this flow with a function, removing all singletons and Contexts.

func IsAdminUser(token string, authService AuthService) int {
userObject := authService.AuthenticateToken(token)
return userObject.IsAdmin() || userObject.IsRoot()
}

This function definition is now a clear representation of what is required to know if a user is an admin. This representation is also apparent to the user of the function and makes refactoring and reusing the function more understandable.

Context.Value and the realities of large systems

Context.Value should inform, not control

To clarify, 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 too heavily. Beyond 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

One example of inform is a request ID. Generally these are used in logging or other aggregation systems to group requests together. The actual contents of a request ID never change the result of an if statement and if a request ID is missing it does nothing to modify the result of a function.

Another example that fits the definition of inform is a logger. The presence or lack of a logger never changes the flow of a program. Also, what is or isn’t logged is usually not documented or relied upon behavior in most uses. However, if the existence of logging or contents of the log are documented in the API, then the logger has moved from inform to control.

Another example of inform is the IP address of the incoming request, if the only purpose of this IP address is to decorate log messages with the IP address of the user. However, if the documentation or expected behavior of your library is that some IPs are more important and less likely to be throttled then the IP address has moved from inform to control because it is now required input, or at least input that alters behavior.

A database connection 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.

The golang.org blog post on context.Context is potentially a counter example of how to correctly use context.Value. Let’s look at the Search code posted in the blog.

func Search(ctx context.Context, query string) (Results, error) {
// Prepare the Google Search API request.
// ...
// ...
q := req.URL.Query()
q.Set(“q”, query)
// If ctx is carrying the user IP address, forward it to the server.
// Google APIs use the user IP to distinguish server-initiated requests
// from end-user requests.
if userIP, ok := userip.FromContext(ctx); ok {
q.Set(“userip”, userIP.String())
}

The primary measuring stick is knowing how the existence of a userIP on the query changes the result of a request. If the IP is distinguished in a log tracking system so people can debug the destination server, then it purely informs and is OK. If the userIP being inside a request changes the behavior of the REST call or tends to make it less likely to be throttled, then it begins to control the likely output of Search and is no longer appropriate for Context.Value.

The blog post also mentions authorization tokens as something that is stored in context.Value. This clearly violates the rules of appropriate content in Context.Value because it controls the behavior of the function and is required input for the flow of your program. Instead, it is better to make tokens an explicit parameter or member of a struct.

Does Context.Value even belong?

Alternatives to Context.Value

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 being very far from each other.

Let’s now show how using the same abstraction we can do the same thing, but not need to abuse Context.Value.

In this example, 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.

In this example, 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.

Why the exception for maintainers

Try not to use context.Value

--

--

Software Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store