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