Go : “Loading Routes From Files with Mux”

Husnur Ridha Syafni
5 min readApr 4, 2021

If you are a backend or API developer and you speak Go, chances are you’ve ever heard Gorilla/Mux before. Mux, as its official Github repo description says, is a “A powerful HTTP router and URL matcher for building Go web servers with 🦍”. It means that a Gorilla can serve a web service . Great!

This story began when I was developing an http proxy for Kubernetes pods to communicate with some external resources. I decided to use Go and Gorilla/Mux to develop it. Mux is lightweight and easy to use, it supports regex for url building, request can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers, and many other advantages, which was not really the deciding factor on why I chose it in the first place to be honest. I chose it because I’ve used it before and I didn’t have the luxury of time to try another library and read through the documentation.

Into the development, I was registering several url groups which group several urls with prefixes. And then I was thinking “Wouldn’t it be nicer if I can just create a router with prefix and load a compilation of url into it? Much like blueprint in flask or expressJs”. I started googling for it. Much to my surprise, there were so few solutions found in Google. Damn. LoL. Maybe I was the only one who had this problem. One of my friend suggested to change to Gin instead. But it’s very unidiomatic and it breaks the standard handler signature, and its context is inexplicably incompatible with context.Context. Someone in Reddit said that. I don’t really understand any of those, and neither I care. For me I just really love a Gorilla that can serve HTTP. (And I think I’ve made myself clear that I didn’t have time to change framework)

I came across this question in Stackoverflow. It did the trick but it wasn’t satisfying enough for me as it felt somewhat unnatural. There were other solutions, but they were pretty similar. Then I said “Looks like I have to deal with it myself!”.

Walking That Extra Miles

My code started out looking somewhat like this. This is just like when you register a url path along with its handler.

package mainimport (
"log"
"net/http"
"time"
"github.com/gorilla/mux"
)
func main() {

log.Println("App Started")

main := mux.NewRouter()
main.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "pong")
}))
server := &http.Server{
Handler: main,
Addr: "127.0.0.1:80",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(server.ListenAndServe())
}

Run the code, and your app will start listening for request. It will return “pong” every time you hit http://localhost/ping. Nothing special. Now the problem is, how do we register a path prefix and then register collections of path that belongs to that path prefix from other files? You don’t want to put tens of URL path in just one file right? You want to separate them and make them more manageable. How do we do that?

My thought was to implement it like middleware chaining to a Mux subrouter. The chain will allow you to register as many URL collections as you like to a router (or subrouter for that matter). Let’s take a look at these code below. I want to register two URL collections to our Mux Router so that it will have three paths

http://localhost/ping                      will return "pong"
http://localhost/hello/ will return "hello"
http://localhost/hello/world will return "hello world"

router.go

router.gopackage routerimport(   "subrouter-test/route"
"github.com/gorilla/mux"
)func LoadURLRoute(r *mux.Router, routes ...route.Route) { for _,s := range routes {
s(r)
}
}

Function router.LoadURLRoute() will take your Mux router along with array of functions in which you can prepare your URL collections. It will basically do a looping through the route functions and put your router as an argument for that function.

route.go

package routeimport(  "github.com/gorilla/mux")type Route func(*mux.Router)func PingPongRoute() Route {  return func(r *mux.Router){
r.HandleFunc("/ping", pingPongHandler)
}
}
func HelloWorldRoute() Route {
return func(r *mux.Router){
hello := r.PathPrefix("/hello").Subrouter()
hello.HandleFunc("/", helloHandler)
hello.HandleFunc("/world", helloWorldHandler)
}
}
func pingPongHandler(w http.ResponseWritter, r *http.Request) {
fmt.Fprintf(w, "pong")
}
func helloHandler(w http.ResponseWritter, r *http.Request) {
fmt.Fprintf(w, "hello")
}
func helloWorldHandler(w http.ResponseWritter, r *http.Request) {
fmt.Fprintf(w, "hello world")
}

In route.go I prepare the route functions that will register our collection of route to the router. There are two route functions as you can see PingPongRoute() and HelloWorldRoute(). Well, I could separate the route function to two separate files if I want, but for this example I will put them inside a same file.

PingPongRoute() will register a path “/ping” that will return “pong”. Nothing too special there. The HelloWorldRoute() on the other hand, I kind of modify it a little bit. I created a subrouter out of main router with prefix “/hello”. Then I register two routes to the subrouter “/” and “/world” which will return “hello” and “hello world” respectively. Again, I could actually create the subrouter in main.go and load the route function to the subrouter. But I just want to show you that you can chain those route functions together.

modify the main.go

import (
...
"subrouter-test/route"
"subrouter-test/router"
)
...main := mux.NewRouter()
router.LoadURLRoute(
main,
route.PingPongRoute(),
route.HelloWorldRoute(),
)
...

Now I modify the main.go a little bit to put LoadURLRoute function to work. I add the necessary package to import section, and the rest is left untouched. Run the code and your app will start listening for request and have three routes “/ping”, “/hello/”, “/hello/world”. Ta-da!

As Frieza Would Say: “This isn’t Even My Final Form”

Remember when I said that the solutions from stackoverflow was unnatural? What do you think I meant by that? let’s take a look at the problem one more time.

“Wouldn’t it be nicer if I can just create a router with prefix and load a compilation of url into it? Much like blueprint in flask or express in nodeJs”.

The solution I write above won’t satisfy this exact need. I need a function that can help me load the URLs and only attribute it to a router. For that, I need to use a method instead. So let’s do that.

Wait.

There’s a problem.

How do I do that without Go compiler constantly shouting “cannot define new methods on non-local type mux.Router” at my face?

Well, we could use an embedded type to do that. Embedding a type inside a type is like creating inheritance in the world of object oriented programming. Except, one does not simply use embedded type and expect full inheritance of a type. What do you think this is, Java? LoL. This article from Ralph Ligtenberg covers all the myths about Type Embedding. Have a look at it.

So let’s modify our router.go file a little bit.

package router
type Router struct {
*mux.Router
}
func (r *Router) LoadURLRoute(routes ...route.Route){
for _, s := range routes {
s(r.Router)
}
}

Now, modify main.go

import (
...
"subrouter-test/route"
"subrouter-test/router"
)
...mux := mux.NewRouter()
main := &route.Router{mux} //type embedding
main.LoadURLRoute(
route.PingPongRoute(),
route.HelloWorldRoute(),
)
...

Now, the way we can register URLs looks more natural because it can only be attributed to a router type. Voila!

I think that’s all I can share about my problem, albeit unnecessary for some people. But, as Ricky Gervais would say, “I don’t care”.

ciao!

--

--