Bouke van der Bijl

Adding some context to Go

Go 1.7 was just released, and it contains a very useful addition to the standard library: the context package! Context is a pattern that is used for passing down request-scoped values and timeouts to Goroutines that are involved with a request. The Go blog has a useful article with examples on how to use Context. Like any worker with a new hammer, I immediately started looking for nails to hit.

One of my favourite Go packages for writing HTTP services is Julian Schmidt’s httprouter. It provides you with a no-nonsense way to have parameterized routing in Go, while having better performance than the built-in muxer. Using httprouter looks something like this (example stolen from the README):

package main

import (
    "fmt"
    "github.com/julienschmidt/httprouter"
    "net/http"
    "log"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    log.Fatal(http.ListenAndServe(":8080", router))
}

As you can see the API is very straightforward, but it does come at a price. You now need to add a third parameter to your handlers, which makes it API-incompatible with the built-in http.HandlerFunc type definition. Another issue is that middlewares don’t pass through the parameters. That is, if there is anything between the httprouter and your handler (like a middleware that compresses the response), you have no way of accessing the URL parameters.

Enter Context

Context seems to be the perfect fit for this. It allows us to attach additional information to the request, without forcing mid/downstream consumers to change their API. In Go 1.7 the http.Request struct has two new methods;

func Context() context.Context

and

WithContext(ctx context.Context) *Request

the latter of which returns a new Request with the context replaced. I have forked httprouter and attempted to re-implement the parameters using Context. It uses as much of the standard net/http interfaces as possible, and using it looks as follows:

package main

import (
    "fmt"
    "github.com/bouk/httprouter"
    "net/http"
    "log"
)

func Index(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello, %s!\n", httprouter.GetParam(r, "name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    log.Fatal(http.ListenAndServe(":8080", router))
}

Because we are now conforming to the Go http.Handler interface, we can easily attach a middleware onto a single route, while still retaining the ability to reach the parameters. For example, we could put github.com/NYTimes/gziphandler onto our Hello handler:

package main

import (
	"fmt"
	"github.com/NYTimes/gziphandler"
	"github.com/bouk/httprouter"
	"log"
	"net/http"
)

func Index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hello, %s!\n", httprouter.GetParam(r, "name"))
}

func main() {
	router := httprouter.New()
	router.GET("/", Index)
	router.GET("/hello/:name", gziphandler.GzipHandler(http.HandlerFunc(Hello)))

	log.Fatal(http.ListenAndServe(":8080", router))
}

Other uses for Context

Request-scoped value passing is just one of the use cases of Context, as it also allows for communicating cancellation, deadlines and timeouts. These features make building more resilient services easier, and support for cancelation has already been built into the standard net, net/http and os/exec packages.

Conclusion

I enjoyed adapting an existing library to use this new feature, and I’m excited to see what other people do with it! I would love to get some feedback on my understanding of what/how Context should be used, so feel free to send me a tweet.

Aug 2016