There we talked about one problem with our server, which is that the routing logic is scattered across several places in our program.
This is a problem that everyone who writes HTTP servers without using dependencies is faced with. Unless the server, taking into account the system of its routes, is not an extremely minimalistic design (for example, these are some specialized servers that have only one or two routes), then it turns out that the size and complexity of the organization of the router code is something that experienced programmers pay attention very quickly.
β Improved routing system
The first thought that might occur to someone who decided to improve our server might be the idea of ββabstracting its routing system, perhaps using a set of functions or a data type with methods. There are many interesting approaches to solving this problem, applicable in each specific situation. The Go ecosystem has many powerful third-party libraries that have been used successfully in various projects to implement router capabilities. I highly recommend taking a look at this material, which compares several approaches to handling simple sets of routes.
Before moving on to a practical example, let's remember how the API of our server works:
POST /task/ : ID GET /task/<taskid> : ID GET /task/ : DELETE /task/<taskid> : ID GET /tag/<tagname> : GET /due/<yy>/<mm>/<dd> : ,
In order to make the routing system more convenient, we can do this:
- You can create a mechanism that allows you to define separate handlers for different methods of the same route. For example, a request
POST /task/
should be processed by one handler, and a requestGET /task/
by another. - You can make it so that the route handler is selected based on deeper analysis of the requests than it is now. That is, for example, with this approach, we should be able to indicate that one handler processes a request to
/task/
, and another handler processes a request to/task/<taskid>
with a numeric oneID
. - In this case, the route processing system should simply extract the numeric
ID
from/task/<taskid>
and pass it to the handler in some way convenient for us.
Writing your own router in Go is very easy. This is because you can organize your work with HTTP handlers using layout. But here I will not indulge my desire to write everything myself. Instead, I propose to talk about how to organize a routing system using one of the most popular routers called gorilla / mux .
β Task management application server using gorilla / mux
The gorilla / mux package is one of the oldest and most popular HTTP routers for Go. The word "mux", in accordance with the package documentation , stands for "HTTP request multiplexer" ("mux" has the same meaning in the standard library).
Since this is a package aimed at solving a single highly specialized task, it is very easy to use it. A version of our server that uses gorilla / mux for routing can be found here . Here is the code for defining the routes:
router := mux.NewRouter()
router.StrictSlash(true)
server := NewTaskServer()
router.HandleFunc("/task/", server.createTaskHandler).Methods("POST")
router.HandleFunc("/task/", server.getAllTasksHandler).Methods("GET")
router.HandleFunc("/task/", server.deleteAllTasksHandler).Methods("DELETE")
router.HandleFunc("/task/{id:[0-9]+}/", server.getTaskHandler).Methods("GET")
router.HandleFunc("/task/{id:[0-9]+}/", server.deleteTaskHandler).Methods("DELETE")
router.HandleFunc("/tag/{tag}/", server.tagHandler).Methods("GET")
router.HandleFunc("/due/{year:[0-9]+}/{month:[0-9]+}/{day:[0-9]+}/", server.dueHandler).Methods("GET")
Please note that these definitions alone immediately close the first two items of the above list of tasks that need to be solved to improve the convenience of working with routes. Due to the fact that calls are used in the description of routes
Methods
, we can easily assign different methods for different handlers in one route. Matching templates (using regular expressions) in the ways allows us to easily distinguish
/task/
and
/task/<taskid>
at the top level route description.
In order to deal with the task, which is in the third paragraph of our list, let's look at the use
getTaskHandler
:
func (ts *taskServer) getTaskHandler(w http.ResponseWriter, req *http.Request) {
log.Printf("handling get task at %s\n", req.URL.Path)
// Atoi,
// , [0-9]+.
id, _ := strconv.Atoi(mux.Vars(req)["id"])
ts.Lock()
task, err := ts.store.GetTask(id)
ts.Unlock()
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
renderJSON(w, task)
}
In a route definition, a route
/task/{id:[0-9]+}/
describes a regular expression used to parse a path and assigns an identifier to a "variable"
id
. This "variable" can be accessed by calling the function
mux.Vars
and passing it to it
req
(gorilla / mux stores this variable in the context of each request, and
mux.Vars
is a convenient helper function for working with it).
β Comparison of different approaches to organizing routing
This is what the code reading sequence looks like in the original server version for those looking to understand how a route is processed
GET /task/<taskid>
.
Here's what to read if you want to understand the code that uses gorilla / mux:
When using gorilla / mux, you will not only have to "jump" less through the program text. Here, in addition, you will have to read much less code. In my humble opinion, this is very good in terms of improving the readability of the code. Describing paths when using gorilla / mux is a simple task and requires only a small amount of code to solve. And whoever reads this code will immediately understand how this code works. Another advantage of this approach is that all routes can be seen literally by looking at the code in one place. And, in fact, the routing setup code now looks very similar to the free-form description of our REST API.
I like to use packages like gorilla / mux because they are very specialized tools. They solve one single problem and they do it well. They are not "taken away" into every corner of the project's program code, which means that, if necessary, they can be easily removed or replaced with something else. If you look at the complete codeof the server variant we are talking about in this article, you can see that the scope of the gorilla / mux mechanisms is limited to a few lines of code. If, as the project develops, some limitation is found in the gorilla / mux package that is incompatible with the specifics of this project, the task of replacing gorilla / mux with another third-party router (or with your own router) should be solved quickly and easily.
What router would you use when developing a REST server in Go?