DEV Community

Kevin Burns
Kevin Burns

Posted on • Updated on

Easy Middleware in Go

#go

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode
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)
    }
}

Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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

[...]
Enter fullscreen mode Exit fullscreen mode

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 (1)

Collapse
kevburnsjr profile image
Kevin Burns Author • Edited on

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...