Context

2016 07 11

When it’s released later this month, Go 1.7 will move the x/net/context package to the stdlib, as plain old context. It also attaches a context object to net/http.Requests, and gives you a few helper methods. Dedicated Gophers are probably already familiar with the great introductory blog article by Sameer Ajmani, Go Concurrency Patterns: Context, published all the way back in 2014. If you haven’t read it yet, give it a review before continuing.

Contexts are, among other things, a method of moving information between callsites in a request chain. In many cases this reduces to moving information between middlewares. But what’s appropriate to put in a context? I had an interesting Tweeting conversation with Mat Ryer and others yesterday on the subject, and Francesc Campoy asked me to write something up with the results.

The first thing to understand about passing values through the context is that it’s completely type-unsafe, and cannot be checked by the compiler. In the end it’s a map[interface{}]interface{}, and comes with exactly the same guarantees, i.e. very few. So, if you can avoid using the context to pass information around, you probably should. And I think this is intuitive advice. When we write functions with a lot of parameters, we generally don’t throw up our hands at some threshold and write

func foo(kwargs map[string]interface{}) { // yolo
    // ...
}

But there are some classes of information for which a context is necessary. This is so-called request scoped data, i.e. information that can only exist once a request has begun. Good examples of request scoped data include user IDs extracted from headers, authentication tokens tied to cookies or session IDs, distributed tracing IDs, and so on.

One important property of that data is that it might not be present. So, for example, if your middleware tries to extract an auth token from the context to do some work, be sure to explicitly code the error path when the token isn’t present, e.g. by responding with 401 Unauthorized.

To know if you should use the context, ask yourself if the information you’re putting in there is available to the middleware chain before the request lifecycle begins. A database handle, or a logger, is generally created with the server, not with the request. So don’t use the context to pass those things around; instead, provide them to the middleware(s) that need them at construction time.

// Don't do this.
func MyMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
        db := r.Context().Value(DatabaseContextKey).(Database)
        // Use db somehow
        next.ServeHTTP(w, r)
    })
}

// Do this instead.
func MyMiddleware(db Database, next http.Handler) http.Handler {
    return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
        // Use db here.
        next.ServeHTTP(w, r)
    })
}

In addition to being fragile, not type safe, and uncheckable by the compiler, the former case is more difficult to understand and therefore to test. How are users of MyMiddleware supposed to know what its dependencies are? They’re obliged to read the implementation. Not great. (Shout out to me: this is just an example of the Go Best Practices 2016 Top Tip #9: Make dependencies explicit!)

I think a good rule of thumb might be: use context to store values, like strings and data structs; avoid using it to store references, like pointers or handles. As Sameer pointed out, this isn’t a bulletproof rule: you could make a case for a request-scoped logger, which could go into the context. But it’s a good place to start.