13.2 Customizing routers
HTTP routing
The HTTP routing component is responsible for mapping HTTP requests to a corresponding function or struct
method. The router takes two key pieces of information from incoming requests:
-The user requested path (for example, /user/123,/article/123
), and any query strings or parameters that come with it (for example, ?id=11
)-The HTTP request method (GET, POST, PUT, and DELETE, PATCH, etc.)
The router then forwards the request to the handler function (controller layer) that has been registered with that particular HTTP method and path.
Default routing implementation
In section 3.4, we introduced Go’s http
package in detail, which included how to design and implement routing. Here, we take another look at an example that illustrates the routing process:
func fooHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
The example above calls http
‘s default mux called DefaultServeMux
, implicitly specified by the nil
parameter in the call to http.ListenAndServe
. The http.Handle
function takes two parameters: the first parameter is the resource you want users to access, specified by its URL path (which is stored in r.URL.Path
) and the second argument binds a handler function with this path. The Router has two main jobs:
- To add and store routing information
- To forward requests to a handler function for processing
By default, Go routes are handled with http.Handle
and http.HandleFunc
types, registered by default through the underlying DefaultServeMux.Handle(pattern string, handler Handler)
function. This function maps resource paths to handlers and stores them in a map[string]muxEntry
map. This is the first job that we mentioned above.
When the application is running, the Go server listens to a port. When it receives a tcp connection, it uses a Handler
to process it. As aforementioned, since the Handler
in the example above is nil
, the default router http.DefaultServeMux
is used. Using the map of previously stored routes, DefaultServeMux.ServeHTTP
will dispatch the request to the first handler with a matching path. This is the router’s second job.
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
if h == nil || len(k) > n {
n = len(k)
h = v.h
}
}
Routing with Beego
At present, most Go web applications base their routing on http
‘s default router, however this has several limitations:
- Does not support dynamic routes with parameters, such as
the/user/:UID
- Does not have good support for REST. The access methods cannot be restricted; for instance in the above example, when users access
/foo
, they can use the GET, POST, DELETE, and HEAD HTTP methods, among others. - In large apps, routing rules can become repetitive and cumbersome. Personally, I’ve developed simple web APIs composed of nearly thirty routing rules when in fact, these rules could have been further simplified using method structs.
The Beego framework’s router is designed to overcome these limitations, taking the REST paradigm into consideration and simplifying the storing and forwarding of routes and requests.
Storing routes
To address the first limitation of the default router, we need to be able to support dynamic URL parameters. For the second and third points, we adopt an alternative approach,mapping REST methods to struct methods and routing requests to this struct instead of to handler functions. This way, a forwarded request can be handled according to it’s HTTP method.
Based on the above ideas, we’ve designed two data types: controllerInfo
, which saves the path and the corresponding controllerType
struct as a reflect.Type
type, and ControllerRegistor
, which saves routing information for the specified Beego application.
type controllerInfo struct {
regex *regexp.Regexp
params map[int]string
controllerType reflect.Type
}
type ControllerRegistor struct {
routers []*controllerInfo
Application *App
}
ControllerRegistor’s external interface contains the following method:
func(p *ControllerRegistor) Add(pattern string, c ControllerInterface)
Its detailed implementation is as follows:
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) {
parts := strings.Split(pattern, "/")
j := 0
params := make(map[int]string)
for i, part := range parts {
if strings.HasPrefix(part, ":") {
expr := "([^/]+)"
//a user may choose to override the default expression
// similar to expressjs: ‘/user/:id([0-9]+)’
if index := strings.Index(part, "("); index != -1 {
expr = part[index:]
part = part[:index]
}
params[j] = part
parts[i] = expr
j++
}
}
//recreate the url pattern, with parameters replaced
//by regular expressions. Then compile the regex.
pattern = strings.Join(parts, "/")
regex, regexErr := regexp.Compile(pattern)
if regexErr != nil {
//TODO add error handling here to avoid panic
panic(regexErr)
return
}
//now create the Route
t := reflect.Indirect(reflect.ValueOf(c)).Type()
route := &controllerInfo{}
route.regex = regex
route.params = params
route.controllerType = t
p.routers = append(p.routers, route)
}
Static routing
We’ve implemented dynamic routing in our example above. By default, Go’s http
package supports serving static files with http.FileServer
, which returns a Handler
. Since we have implemented a custom router, we will also need a way of handling static files. Beego’s static folder path is saved in a global variable called StaticDir
, which maps the URL to corresponding paths. The SetStaticPath
‘s implementation can be seen below:
func (app *App) SetStaticPath(url string, path string) *App {
StaticDir[url] = path
return app
}
The application’s static routes can be set like so:
beego.SetStaticPath("/img", "/static/img")
Forwarding routes
We can forward routes based on the forwarding information contained within ControllerRegistor
. The detailed implementation can be seen in the following code snippet:
// AutoRoute
func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
if !RecoverPanic {
// go back to panic
panic(err)
} else {
Critical("Handler crashed with error", err)
for i := 1; ; i += 1 {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
Critical(file, line)
}
}
}
}()
var started bool
for prefix, staticDir := range StaticDir {
if strings.HasPrefix(r.URL.Path, prefix) {
file := staticDir + r.URL.Path[len(prefix):]
http.ServeFile(w, r, file)
started = true
return
}
}
requestPath := r.URL.Path
//find a matching Route
for _, route := range p.routers {
//check if Route pattern matches url
if !route.regex.MatchString(requestPath) {
continue
}
//get submatches (params)
matches := route.regex.FindStringSubmatch(requestPath)
//double check that the Route matches the URL pattern.
if len(matches[0]) != len(requestPath) {
continue
}
params := make(map[string]string)
if len(route.params) > 0 {
//add url parameters to the query param map
values := r.URL.Query()
for i, match := range matches[1:] {
values.Add(route.params[i], match)
params[route.params[i]] = match
}
//reassemble query params and add to RawQuery
r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery
//r.URL.RawQuery = url.Values(values).Encode()
}
//Invoke the request handler
vc := reflect.New(route.controllerType)
init := vc.MethodByName("Init")
in := make([]reflect.Value, 2)
ct := &Context{ResponseWriter: w, Request: r, Params: params}
in[0] = reflect.ValueOf(ct)
in[1] = reflect.ValueOf(route.controllerType.Name())
init.Call(in)
in = make([]reflect.Value, 0)
method := vc.MethodByName("Prepare")
method.Call(in)
if r.Method == "GET" {
method = vc.MethodByName("Get")
method.Call(in)
} else if r.Method == "POST" {
method = vc.MethodByName("Post")
method.Call(in)
} else if r.Method == "HEAD" {
method = vc.MethodByName("Head")
method.Call(in)
} else if r.Method == "DELETE" {
method = vc.MethodByName("Delete")
method.Call(in)
} else if r.Method == "PUT" {
method = vc.MethodByName("Put")
method.Call(in)
} else if r.Method == "PATCH" {
method = vc.MethodByName("Patch")
method.Call(in)
} else if r.Method == "OPTIONS" {
method = vc.MethodByName("Options")
method.Call(in)
}
if AutoRender {
method = vc.MethodByName("Render")
method.Call(in)
}
method = vc.MethodByName("Finish")
method.Call(in)
started = true
break
}
//if no matches to url, throw a not found exception
if started == false {
http.NotFound(w, r)
}
}
Getting started
Using our router design, we can solve the three limitations mentioned earlier. The three main use-cases are:
Registering route handlers:
beego.BeeApp.RegisterController("/", &controllers.MainController{})
Handling dynamic parameters:
beego.BeeApp.RegisterController("/:param", &controllers.UserController{})
Regex matching:
beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{})
Links
- Previous section: Project planning
- Next section: Designing controllers