drio

Golang context (values)

The final practical use of context is to store data so different components of our application can access to it. As Jon mentions in his book:

idiomatic Go favors the explicit over the implicit, and this includes explicit data passing.

We cannot always pass data explicitly. Common cases are HTTP requests handlers and the associated middleware. The only way to make a value accessible to your middleware is to store it in a context.

Another concrete example that is very common is to pass and maintain a GUID on all the requests in a distributed system. The idea here is the request may spawn multiple services and we want to be able to map a particular service and GUID to a request. What you do is extract the GUID from the http header (or add it if not available) and stick it to the context.

The context value example from Jon's book has a lot of meat. So in this post I am going to "read the code out loud". Let's begin.

func main() {
	bl := BusinessLogic{
		RequestDecorator: tracker.Request,
		Logger:           tracker.Logger{},
		//Remote:           "http://www.example.com/query",
		Remote: "https://www.httpbin.org/get",
	}

	c := Controller{
		Logic: bl,
	}

	r := chi.NewRouter()
	// tracker.Middleware: add GUID to the context (from HEADER or create a new one)
	// identity.Middlware: extract user from cookie and add it to context (if cookie not found stop request)
	r.Use(tracker.Middleware, identity.Middleware)
	r.Get("/", c.handleRequest)
	http.ListenAndServe(":3000", r)
}

First he wraps a few bits of logic in the Controller. The logic property in the Controller is a struct with a few pieces of functionality: a request, a logger and a string that points to the url we will be making the request to. The code uses the chi router. I wasn't aware of that router but I am definitely start using it in my services. The rest of the main function creates the router, adds a couple of pieces of middleware and maps get root requests to a handler. Finally starts the http server. Notice how easily you can attach the middleware to your server. The server will run the two pieces of middleware prior to serve the request. Let's look at what those middleware do.

func Middleware(h http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		ctx := req.Context()
		if guid := req.Header.Get("X-GUID"); guid != "" {
			ctx = contextWithGUID(ctx, guid)
		} else {
			ctx = contextWithGUID(ctx, uuid.New().String())
		}
		req = req.WithContext(ctx)
		h.ServeHTTP(rw, req)
	})
}

The other piece in the tracker package is the Logger. It is just an empty struct that defines a Log method which grabs the guid from the context and prints it in the console.

Finally, to finish with the tracker package, there is a Request function. The signature is a single

The tracker package defines a Middleware and Logger. The Middleware looks for the "X-GUID" in the http header and adds it to the request context. If we cannot find it in the http header, we create a new uuid. Finally we wrap the request with the new context and run the next handler.

The Logger is just an empty struct that has a Log() method defined. That method prints some information to the console, include the UUID.

Finally that package defines a Request function that extracts the context from a request, and if the context has a "X-GUID" key, it will add it to the request header and return it. We use that function in the business logic to add the X-GUID to our http response.

We have covered the main function of the main package and the tracker package. Let's move on.

In the main package we find a few other type definitions before we get to the business logic and the request handler. First, a RequestDecorator which is a function that receives a request and returns a request. Then we have a Logger interface with a single method: Log(context.Context, string). The Logic interface defines a single method businessLogic. And finally, the type Controller is an struct with a single field of type Logic.

And now we are ready for the final bits in that main package. The root handler grabs the context first and uses the UserFromContext to extract the value of the user cookie from the session. If the cookie is not present we return a 500. If not, we continue by extracting the value of the data param from the request and run the business in the controller passing the context, data and user. If all goes well we return the result if not we return a 500.

The businessLogic() uses the logger to log some information and prepares a new request to the bl.Remote using the data as the value of the query param. If all goes well, we use the decorator to add the UUID to the header before making the request. If all goes well we return the result back to the handler which in turn sends that back to the client.

The final piece is the identity middleware that is defined in the identity package by the Middleware function. As mentioned, it looks for the user cookie in the session and stops the request if it cannot find it or adds it to the context and executes the next handler.

All and all this is a very complete example that exercises some important components we use when building http services in golang: using context to store values so we can access it at different middleware levels, using a third party router to facilitate the route creation and then two concrete middleware examples to look for cookies and "mark" requests with UUIDs. Very helpful. Let's see it in action:

➜ curl -v --cookie "user=drio" http://localhost:3000/
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.86.0
> Accept: */*
> Cookie: user=drio
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Tue, 21 Mar 2023 16:40:27 GMT
< Content-Length: 361
< Content-Type: text/plain; charset=utf-8
<
{
  "args": {
    "query": ""
  },
  "headers": {
    "Accept-Encoding": "gzip",
    "Host": "www.httpbin.org",
    "User-Agent": "Go-http-client/2.0",
    "X-Amzn-Trace-Id": "Root=1-6419ddfb-61436a7d5414ff450a5ba892",
    "X-Guid": "f4e3e34e-49df-418a-acd0-6f0aad6ccd9b"
  },
  "origin": "x.x.x.x",
  "url": "https://www.httpbin.org/get?query="
}
* Connection #0 to host localhost left intact

As a final comment, let me drive your attention to this function:

func UserFromContext(ctx context.Context) (string, bool) {
	user, ok := ctx.Value(key).(string)
	return user, ok
}

In the second line we are doing a type assertion because the type of the values we store in the context are interface{} (any type). By doing the assertion we can check if the value is of a specific type, if so, the ok value will be true. I will cover type assertions in future posts.