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{})

results matching ""

    No results matching ""