back to listing index

How to correctly use context.Context in Go 1.7 – Jack Lindamood – Medium

[web search]
Original source (medium.com)
Tags: context golang go
Clipped on: 2017-09-06

Image (Asset 1/7) alt= 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.

Image (Asset 2/7) alt=
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

I strongly empathize with the desire to shove items in Context.Value. Complex systems often have middleware layers and multiple abstractions along the call stack. 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 or auth token. Imagine if you had to add another parameter called “user ID” to dozens of functions between two calls in two different packages just to let package Z know about what package A found out? The API would look ugly and people would yell at you for designing it. GOOD! Just because you’ve taken that ugliness and obscured it inside Context.Value doesn’t make your API or design better. Obscurity is the opposite of good API design.

Context.Value should inform, not control

Inform, not control. This is the primary mantra that I feel should guide if you are using context.Value correctly. The content of context.Value is for maintainers not users. It should never be required input for documented or expected results.

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?

Context does two very different things: one of them times out long running operations and the other carries request scoped values. Interfaces in Go should be about describing behaviors an API wants. They should not be grab bags of functions that happen to often exist together. It is unfortunate that I’m forced to include behavior about adding arbitrary values to an object when all I care about is timing out runaway requests.

Alternatives to Context.Value

People often use Context.Value in a broader middleware abstraction. 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. Note Go 1.7 includes a context on the http.Request object. Also, I’m a bit loose with the syntax, but I hope the meaning is clear.

1 package goexperiments
2
3 import (
4 "context"
5 "net/http"
6 )
7
8 type HandlerMiddleware interface {
9 HandleHTTPC(ctx context.Context, rw http.ResponseWriter, req *http.Request, next http.Handler)
10 }
11
12 var function1 HandlerMiddleware = nil
13 var function2 HandlerMiddleware = nil
14
15 func addUserID(rw http.ResponseWriter, req *http.Request, next http.Handler) {
16 ctx := context.WithValue(req.Context(), "userid", req.Header.Get("userid"))
17 req = req.WithContext(ctx)
18 next.ServeHTTP(rw, req)
19 }
20
21 func useUserID(rw http.ResponseWriter, req *http.Request, next http.Handler) {
22 uid := req.Context().Value("userid")
23 rw.Write([]byte(uid))
24 }
25
26 func makeChain(chain ...HandlerMiddleware) http.Handler {return nil}
27
28 type Server struct {}
29 func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
30 req = req.WithContext(context.Background())
31 chain := makeChain(addUserID, function1, function2, useUserID)
32 chain.ServeHTTP(rw, req)
33 }

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.

1 package goexperiments
2
3 import (
4 "context"
5 "net/http"
6 )
7
8 type HandlerMiddleware interface {
9 HandleHTTPC(ctx context.Context, rw http.ResponseWriter, req *http.Request, next http.Handler)
10 }
11
12 var function1 HandlerMiddleware = nil
13 var function2 HandlerMiddleware = nil
14
15
16 func makeChain(chain ...HandlerMiddleware) http.Handler {return nil}
17
18 type AddUserID struct {
19 OnUserID func(userID string) http.Handler
20 }
21
22 func (a *AddUserID) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
23 userID := req.Header.Get("userid")
24 a.OnUserID(userID).ServeHTTP(rw, req)
25 }
26
27 type UseUserID struct {
28 UserID string
29 }
30
31 func (u *UseUserID) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
32 rw.Write([]byte(u.UserID))
33 }
34
35 type ServerNoAbuseContext struct{}
36
37 func (s *ServerNoAbuseContext) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
38 req = req.WithContext(context.Background())
39 chainWithAuth := func(userID string) http.Handler {
40 return makeChain(function1, function2, &UseUserID{
41 UserID: userID,
42 })
43 }
44 chainPartOne := &AddUserID{
45 OnUserID: chainWithAuth,
46 }
47 chainPartOne.ServeHTTP(rw, req)
48 }

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

I admit carving out an exception for maintainers in Context.Value is somewhat arbitrary. My personal reasoning is to imagine a perfectly designed system. In this system, there would be no need for introspecting an application, no need for debug logs, and little need for metrics. The system is perfect so maintenance problems don’t exist. Unfortunately, the reality is that we do need to debug systems. Putting this debug information in a Context object is a compromise between the perfect API that would never need maintenance and the realities of wanting to thread debug information across an API. However, I wouldn’t particularly argue with someone that wants to make even debugging information explicit in their API.

Try not to use context.Value

You can get into way more trouble than it’s worth trying to use context.Value. I empathize about how easy it is to just add something into context.Value and retrieve it later in some far away abstraction, but the ease of use now is paid for by pain when refactoring later. There is almost never a need to use it and if you do, it makes refactoring your code later very difficult because it becomes unknown (especially by the compiler) what inputs are required for functions. It’s a very contentious addition to Context and can easily get one in more trouble than it’s worth.

Show your support

Clapping shows how much you appreciated Jack Lindamood’s story.

  • Image (Asset 3/7) alt=