Not sure how to structure your Go web application?

My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

Context-Aware Handler Chains in Go (using Stack)

Published on:

As of Go 1.7 the context package is now part of Go's standard library and you should use that to pass data between your handlers. Joe Shaw has made a good tutorial here. If you're using a Go version < 1.7 then the information below may still be useful.

I've written a package for chaining context-aware handlers in Go, called Stack (view it on GitHub). It was heavily inspired by Alice.

What do you mean by 'context-aware'?

If you're using a middleware pattern to process HTTP requests in Go, you may want to share some data or context between middleware handlers and your application handlers. For example you might want to:

  • Use some middleware to create a CRSF token, and later render the token to a template in your application handler. Or perhaps...
  • Authenticate a user in one middleware handler, and then pass the user details to a second middleware handler which checks if the user is authorised to access the resource.

There are a few packages that can help with this. Matt Silverlock has written a good article about some of the different approaches and tools – I won't rehash it here, instead I recommend giving it a read.

Why make another package?

Because none of the existing tools seemed ideal – at least to me. gorilla/context is simple and very flexible, but relies on a global context map and you remembering to clear the context after each request. (It's still my favourite though). goji provides request-scoped context, which is good, but it's part of a larger package and ties you into using the Goji router. The same is true of gocraft/web, which also relies on reflection tricks under the hood that I struggle to wrap my head around.

I realised that the only time you need to worry about context is when you're chaining handlers together. So I looked at my favorite tool for chaining handlers, Alice, and began adapting that to create Stack.

I wanted the package to:

  • Do a simple job, and then get out of the way.
  • Provide a request-scoped context map.
  • Let you create stackable, reusable, handler chains in the Alice style.
  • Be as type-safe at compile time as it possibly could be.
  • Be simple to understand and non-magic.
  • Operate nicely with existing standards. In particular:
    • The handler chain must satisfy the http.Handler interface, so it can be used with the http.DefaultServeMux.
    • It should be compatible with the func(http.Handler) http.Handler pattern commonly used by third-party middleware packages.

The full documentation for Stack is here, but here's a quick example of how to use it:

File: main.go
package main

import (
    "fmt"
    "github.com/alexedwards/stack"
    "github.com/goji/httpauth"
    "net/http"
)

func main() {
    // Setup goji/httpauth, some third-party middleware
    authenticate := stack.Middleware(httpauth.SimpleBasicAuth("user", "pass"))

    // Create a handler chain and register it with the DefaultServeMux
    http.Handle("/", stack.New(authenticate, tokenMiddleware).Then(tokenHandler))
    http.ListenAndServe(":3000", nil)
}

func tokenMiddleware(ctx stack.Context, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Add a value to Context with the key 'token'
        ctx["token"] = "c9e452805dee5044ba520198628abcaa"
        next.ServeHTTP(w, r)
    })
}

func tokenHandler(ctx stack.Context) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Retrieve the token from Context and print it
        fmt.Fprintf(w, "Token is: %s", ctx["token"])
    })
}
$ curl -i user:pass@localhost:3000
HTTP/1.1 200 OK
Content-Length: 41
Content-Type: text/plain; charset=utf-8

Token is: c9e452805dee5044ba520198628abcaa
$ curl -i user:wrongpass@localhost:3000
HTTP/1.1 401 Unauthorized
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Www-Authenticate: Basic realm="Restricted"

Unauthorized