Deepjyoti Barman
Posted on July 16, 2023
Go is by far one of the most interesting languages that I have worked with and I have enjoyed working with it almost everytime (except when functions do not support default values). Recently, I came across this problem where I had a use-case to support user defined routes for an API. Problem was supporting these user defined URL's in real time and actually triggerring a handler based on them.
More details of the problem
The use-case I had was that the users will be allowed to define any custom route path and some handler logics that should be executed based on that. It's easy to handle the part of triggering the user defined logic based on the route matched. Hard part is to check whether an incoming request is supposed to be matched against an user-defined route or a system route.
Naive Approach that was considered
I considered this naive approach where I would restart the router (using Mux) everytime an user defined route is added/deleted/updated and the router will know whether the incoming request is for an user defined route or system.
This approach is simple enough but is a very bad practice. Imagine thousands of users updating their routes every now and then and the whole router restarts and the API (as a whole) goes down for a few seconds. This can be a major problem for a production API that is customer facing.
Thus, there was need for figuring out an alternate approach to this problem that would be efficient as well.
How it's Solved
Just a heads up, before I dive into details, the problem was solved by using mux
and it's useful functions (thanks to the developers for considering a scenario like this while developing).
In order to understand the solution to the problem, we will have to understand how mux
handles an incoming request. The flow is important here in order to solve this problem in a very efficient way.
In mux
, every route has two properties (or functions) that can be defined against it. One of them is a matcher
which can be defined optionally but not necessarily required. The other is a handler
which is the actual function that will be trigerred if the route is matched.
Matchers
mux
uses matchers to determine whether a route should match. matchers
are just mere functions which return a boolean value. This value indicates whether the route is matched or not matched.
-
matcher
returnstrue
: route is matched and handler should be called. -
matcher
returnsfalse
: route is not matched
NOTE: One thing to know here is that
mux
goes through each route that is defined in the router until amatcher
returnstrue
or the end of routes is reached. When end of routes is reached, a 404 is returned with an error indicating the page is not found.
Matchers are a boon in this particular scenario as this makes solving dynamic user defined routes really simple.
Here's an example matcher that matches the incoming route if it is /hello
:
func getHelloMatcher() mux.MatcherFunc {
return func(req *http.Request, rm *mux.RouteMatch) bool {
return req.URL.Path == "/hello"
}
}
Above is a very simple example of how a custom matcher can be defined but in order to match against a dynamic route, it is better to use mux
's own functions. mux
internally uses a Match
function that checks if a route matches the incoming requests path and accordingly returns true
or false
. It is ideal for us to use this function as they use some complex regex to match the path.
Following code shows how to use mux
's internal Match
function:
func getHelloMatcher() mux.MatcherFunc {
return func(req *http.Request, rm *mux.RouteMatch) bool {
// In order to use a internal `mux` function we will need to
// define a dummy router that can provide the function.
copyMuxRouter := mux.NewRouter().StrictSlash(true)
return copyMuxRouter.Methods(http.MethodGet, http.MethodPost).
Name("dynamic router").
Path("/hello").
Match(req, rm)
}
}
As explained, above code uses an internal function to match the incoming route against a route /hello
. This part of the code can be made dynamic based on user defined route that can be fetched accordigly.
Handlers
After a matcher
returns true
, the route's handler
is called. In mux
, all routes will absolutely need a handler
defined against it or else mux
will not start the router and throw an error at startup.
Pretty much anyone who has used mux
is aware of how to define a handler so I will not go over the steps on that. Here's a full example from mux docs on how to define a handler for a route.
Enhancements to the above
The problem that I initially mentioned can be solved by using matchers
as I have explained. However, there are certain places where the code can be made efficient. The matcher
function that mux
supports, uses a RouteMatch
type. This is a custom type inside of which there's a Vars
key which maps to a map
type. This Vars
can be used to pass details from the matcher
to the handler
in order to reduce some redundant steps.
As an example, say the user defined route is stored in a database and the user can also define different methods for the route. These details might come handy in the handler
. In our scenario, these details are definitely required in the matcher
phase of the code where a database call can be made to get the details for a route.
In the handler
phase of the flow, instead of making another database call to get the details of the matched route, those details can simply be passed from the matcher
to the handler
by using the Vars
of the RouteMatch
object.
I am not sure if RouteMatch.Vars
was intentionally designed to do that but it is definitely a good hack (if you will) to make the code efficient and relatively faster (database network calls are expensive).
Following example shows how some details are passed from the matcher
to the handler
:
func getHelloMatcher() mux.MatcherFunc {
return func(req *http.Request, rm *mux.RouteMatch) bool {
// In order to use a internal `mux` function we will need to
// define a dummy router that can provide the function.
copyMuxRouter := mux.NewRouter().StrictSlash(true)
methods := [http.MethodGet, http.MethodPost]
isMatched := copyMuxRouter.Methods(methods...).
Name("dynamic router").
Path("/hello").
Match(req, rm)
if isMatched {
rm.Vars["MATCHED_ROUTE_METHODS"] = strings.Join(methods, "-")
}
return isMatched
}
}
Following is how to use the MATCHED_ROUTE_METHODS
in the handler
:
func getHelloHandler() mux.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
// Read the vars
vars := mux.Vars(req)
// Get the methods.
methods := vars["MATCHED_ROUTE_METHODS"]
fmt.Println("Matched methods are: ", methods)
}
}
GoLang is becoming one of my favorite languages to work on and I am very close to even replacing Python with GoLang in my next project. I think everyone should give it a try to find out the beauty of it.
gorrila/mux
team has been doing a great job building this awesome router. Go give it a star at gorilla/mux.
This post was originally posted at my blog at https://blog.deepjyoti30.dev
Posted on July 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.