Modern Golang MUX
Mar 2, 2019Lately I’ve been noticing that new Golang web-frameworks have their own way of defining HTTP handlers. They differ in syntax, but the common theme is that the handler usually accepts some sort of context where request and other data are linked:
// Echo
func(c echo.Context) error {}
// Buffao
func (c buffalo.Context) error {}
// Gin
func(c *gin.Context) {}
// Macaron
func(ctx *macaron.Context) {}
These are just a few of them and the problem here is that this locks you in on that specific framework and diverges from idiomatic way of doing handlers in Go. Now I understand how this came to be, prior to Go 1.7 Context wasn’t baked in with the language and so people found creative ways to pass additional data that are per-request context. But now that we have Context and per-request context via Request.WithContext and Request.Context, its about time we fix this.
Go has two ways to define a handler, if you have a struct, you just need to implement the Handler interface and you can pass your struct instance as a handler:
type MyHandler struct {}
func (h *MyHandler) ServeHTTP(ResponseWriter, *Request) {}
// usage
http.Handle("/", new(MyHandler))
Or if you have a function, you just need to have the signature HandlerFunc and you can pass your function as a handler as well:
myHandler := func(w http.ResponseWriter, r *http.Request) {}
// usage
http.HandleFunc("/", myHandler)
By following these interface, you make your code compatible against packages that follow idiomatic Go and makes code from Go ecosystem highly reusable.
Now its not all bad, I still found others that follow these interface like Goji and httptreemux and there are possibly more but I want to highlight some of httptreemux
features that makes it awesome.
First is the handler:
func(w http.ResponseWriter, r *http.Request) {}
Oh yeah, exactly like what http.HandleFunc expects. So this sets you up for reusability right off the bat.
What about url namespacing?
router := httptreemux.NewContextMux()
router.GET("/:page", func(w http.ResponseWriter, r *http.Request) {
params := httptreemux.ContextParams(r.Context())
fmt.Fprintf(w, "GET /%s", params["page"])
})
group := router.NewGroup("/api")
group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request) {
// do some magic
})
It can’t get any better than this! See how params are nicely tucked into the request’s context, its just cleaner this way.
Sensible yet customizable defaults for routing, and trailing slash handling just makes me happy.
But does it perform well? I opened this PR to benchmark against other MUX and here’s the result:
GocraftWeb_Simple-4 3000000 491 ns/op 6 allocs/op
GocraftWeb_Route15-4 1000000 1931 ns/op 7 allocs/op
GocraftWeb_Route75-4 1000000 2117 ns/op 7 allocs/op
GocraftWeb_Route150-4 1000000 2222 ns/op 7 allocs/op
GocraftWeb_Route300-4 1000000 2048 ns/op 7 allocs/op
GocraftWeb_Route3000-4 500000 2720 ns/op 7 allocs/op
GocraftWeb_Middleware-4 2000000 897 ns/op 8 allocs/op
GocraftWeb_Composite-4 500000 3488 ns/op 8 allocs/op
GorillaMux_Simple-4 2000000 867 ns/op 9 allocs/op
GorillaMux_Route15-4 500000 2500 ns/op 10 allocs/op
GorillaMux_Route75-4 300000 4662 ns/op 10 allocs/op
GorillaMux_Route150-4 200000 7343 ns/op 10 allocs/op
GorillaMux_Route300-4 100000 14347 ns/op 10 allocs/op
GorillaMux_Route3000-4 10000 121817 ns/op 12 allocs/op
Martini_Simple-4 300000 3869 ns/op 10 allocs/op
Martini_Route15-4 300000 4867 ns/op 10 allocs/op
Martini_Route75-4 200000 6153 ns/op 10 allocs/op
Martini_Route150-4 200000 7788 ns/op 10 allocs/op
Martini_Route300-4 100000 11112 ns/op 10 allocs/op
Martini_Route3000-4 20000 117156 ns/op 12 allocs/op
Martini_Middleware-4 100000 15124 ns/op 16 allocs/op
Martini_Composite-4 100000 19231 ns/op 17 allocs/op
PiluTraffic_Simple-4 1000000 1197 ns/op 13 allocs/op
PiluTraffic_Route15-4 1000000 1919 ns/op 18 allocs/op
PiluTraffic_Route75-4 500000 3202 ns/op 26 allocs/op
PiluTraffic_Route150-4 300000 4881 ns/op 37 allocs/op
PiluTraffic_Route300-4 200000 8152 ns/op 58 allocs/op
PiluTraffic_Route3000-4 20000 84404 ns/op 423 allocs/op
PiluTraffic_Middleware-4 1000000 1091 ns/op 13 allocs/op
PiluTraffic_Composite-4 300000 4948 ns/op 39 allocs/op
HttpTreeMux_Simple-4 20000000 106 ns/op 0 allocs/op
HttpTreeMux_Route15-4 3000000 446 ns/op 3 allocs/op
HttpTreeMux_Route75-4 3000000 453 ns/op 3 allocs/op
HttpTreeMux_Route150-4 3000000 455 ns/op 3 allocs/op
HttpTreeMux_Route300-4 3000000 468 ns/op 3 allocs/op
HttpTreeMux_Route3000-4 3000000 527 ns/op 3 allocs/op
Aside from consistent routing speed, notice how it barely allocates memory as well compared to others. With a modern mux like this, I’m excited for the future of web-frameworks in Golang and maybe perhaps I’d write one myself just for func!