loading...

Easy Middleware in Go

#go
kevburnsjr profile image Kevin Burns Updated on ・3 min read

Negroni and Alice are great and you should probably use one of those.
But if you're weird like me and you want to try your hand at rolling a bespoke HTTP middleware stack instead, you might find this pattern useful.
Full source in the gist
So here's our main function

package main

import (
    "net/http"
)

func main() {
    dh := DispatchHandler{}
    dh.Attach(DontCache)
    dh.Attach(Timer)
    dh.Finalize(Router)

    h := &http.Server{Addr: ":80", Handler: dh}
    h.ListenAndServe()
}

We instantiate a dispatch handler, attach some middleware, and finalize it with an http handler function.
DispatchHandler contains a middleware stack which is compiled down into a final function which is what gets called for each request.

type DispatchHandler struct {
    stack []func(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request)
    final func(w http.ResponseWriter, r *http.Request)
}
func (h *DispatchHandler) Attach(m func(func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request)) {
    h.stack = append(h.stack, m)
}
func (h *DispatchHandler) Finalize(final func(w http.ResponseWriter, r *http.Request)) {
    h.final = final
    for i := len(h.stack); i > 0; i-- {
        h.final = h.stack[i-1](h.final)
    }
}
func (h DispatchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    h.final(w, r)
}

Attach accepts a function which accepts a function and returns a function. This might seem hard to follow but it's very similar to the way Rack or WSGI work in ruby and python. You create a function which accepts the next function down the call stack and it decides whether and when it will call that next function down the stack.

// This piece of middleware just sets an HTTP response header to prevent rogue caches 
// from caching any responses we haven't explicitly marked as cacheable. 
// Subsequent calls to w.Header().Set("Cache-Control" ...) will override this value
func DontCache(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Cache-Control", "max-age=0, no-cache, must-revalidate")
        next(w, r)
    }
}
import (
    "time"
    "log"
)

// This piece of middleware logs the response time for every request
func Timer(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next(w, r)
        log.Printf("%d %s", time.Since(start).Nanoseconds() / 1e3, r.URL.Path)
    }
}

Finalize accepts an http handler to cap off the stack. It takes the slice of functions we attached and sandwiches them all together into one callable function. In this case, we're passing a simple Router as the last function in the stack.

var routes = map[string]func(http.ResponseWriter, *http.Request){
    "/":      IndexHandler,
    "/hello": HelloHandler,
    "/world": WorldHandler,
    "/panic": PanicHandler,
}
func Router(w http.ResponseWriter, r *http.Request) {
    if handle, ok := routes[r.URL.Path]; ok {
        handle(w, r)
    } else {
        http.Error(w, "Not Found : "+r.URL.Path, 404)
    }
}
func IndexHandler(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "Hello, World!", 200)
}
func HelloHandler(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "Hello", 200)
}
func WorldHandler(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "World", 200)
}
func PanicHandler(w http.ResponseWriter, r *http.Request) {
    log.Panic("REM WAS RIGHT")
}

Notice we added a panic route. To help understand how this works, we're going to hit that route and have a look at the resulting stack trace:

2017/07/23 17:34:57 http: panic serving 172.28.128.1:50552: REM WAS RIGHT
goroutine 11 [running]:

[...]

main.Router(0x797b20, 0xc4200e02a0, 0xc42000ab00)
        /var/www/middleware/main.go:65 +0x8b
main.Timer.func1(0x797b20, 0xc4200e02a0, 0xc42000ab00)
        /var/www/middleware/main.go:50 +0x7d
main.DontCache.func1(0x797b20, 0xc4200e02a0, 0xc42000ab00)
        /var/www/middleware/main.go:43 +0x9d
main.DispatchHandler.ServeHTTP(0xc420010eb0, 0x2, 0x2, 0xc420010ed0, 0x797b20, 0xc4200e02a0, 0xc42000ab00)
        /var/www/middleware/main.go:35 +0x44

[...]

See how the stack trace shows the way we add middleware to the call stack?
Pretty neat.
Here's the gist: https://gist.github.com/KevBurnsJr/0ae92357d2e64d84b8a857764361c9c2

Discussion

pic
Editor guide
Collapse
kevburnsjr profile image
Kevin Burns Author

A reddit user by the name of TheMeruvious has recommended a simpler approach which also works without the additional DispatchHandler struct.
gist.github.com/KevBurnsJr/2080536...

Full discussion on reddit
reddit.com/r/golang/comments/6p46s...