-1
\$\begingroup\$

in my jurney to learn Go, I decided to write a simple router which I called it Gouter, which I think it has most of the features in gorilla/mux but in my opinion it's easier to use. Anyway, it consists of two file, router.go and route.go. There are some concerns I have about this. First about the performance and the second is, is it good enough to use it in production or not? and the finally how can I improve it.

Thanks

router.go

package router

import (
    "context"
    "net/http"
)

type key int

const (
    contextKey key = iota
    varsKey
)

type Router struct {
    // Routes stores a collection of Route struct
    Routes []Route

    // ctx is an interface type will be accessible from http.request
    ctx    interface{}
}


// NewRouter return a new instance of Router
func NewRouter() *Router {
    return &Router{}
}

// GET register a GET request
func (r *Router) GET(path string, h http.HandlerFunc) *Route {
    return r.AddRoute(path, http.MethodGet, h)
}

// POST register a POST request
func (r *Router) POST(path string, h http.HandlerFunc) *Route {
    return r.AddRoute(path, http.MethodPost, h)
}

// PUT register a PUT request
func (r *Router) PUT(path string, h http.HandlerFunc) *Route {
    return r.AddRoute(path, http.MethodPut, h)
}

// PATCH register a PATCH request
func (r *Router) PATCH(path string, h http.HandlerFunc) *Route {
    return r.AddRoute(path, http.MethodPatch, h)
}

// DELETE register a DELETE request
func (r *Router) DELETE(path string, h http.HandlerFunc) *Route {
    return r.AddRoute(path, http.MethodDelete, h)
}

// AddRoute create a new Route and append it to Routes slice
func (r *Router) AddRoute(path string, method string, h http.HandlerFunc) *Route {
    route := NewRoute(path, method, h)
    r.Routes = append(r.Routes, route)
    return &route
}

// With send an interface along side the http.request.
// It is accessible with router.Context() function
func (r *Router) With(i interface{}) *Router {
    r.ctx = i
    return r
}

// ServeHTTP implement http.handler
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctx := context.WithValue(req.Context(), contextKey, r.ctx)
    req = req.WithContext(ctx)

    var match *Route
    var h http.Handler
    for _, route := range r.Routes {
        if route.Match(req) {
            vars := route.extractVars(req)
            ctx := context.WithValue(req.Context(), varsKey, vars)
            req = req.WithContext(ctx)
            match = &route
            break
        }
    }

    if match != nil && match.method != req.Method {
        h = &MethodNotAllowed{}
    }

    if h == nil && match != nil {
        h = match.dispatch()
    }

    if match == nil || h == nil {
        h = http.NotFoundHandler()
    }

    h.ServeHTTP(w, req)
}

// Vars return a map of variables defined on the route.
func Vars(req *http.Request) map[string]string {
    if v := req.Context().Value(varsKey); v != nil {
        return v.(map[string]string)
    }
    return nil
}

func Context(req *http.Request) interface{} {
    if v := req.Context().Value(contextKey); v != nil {
        return v
    }
    return nil
}

and the route.go:

package router

import (
    "fmt"
    "net/http"
    "regexp"
    "strings"
)

type Middleware func(handler http.Handler) http.Handler

type Route struct {
    path    string
    name    string
    handler http.Handler
    method  string
    mw      []Middleware
    where   map[string]string
    vars    map[string]string
}

// NewRoute create a new route
func NewRoute(path string, method string, handler http.HandlerFunc) Route {
    return Route{
        path:    path,
        handler: handler,
        method:  method,
        vars:    make(map[string]string),
        where:   make(map[string]string),
    }
}

// Name assign a name for the route
func (r *Route) Name(s string) *Route {
    r.name = s
    return r
}

// Match return true if the requested path would match with the current route path
func (r *Route) Match(req *http.Request) bool {
    regex := regexp.MustCompile(`{([^}]*)}`)
    matches := regex.FindAllStringSubmatch(r.path, -1)
    p := r.path
    for _, v := range matches {
        s := fmt.Sprintf("{%s}", v[1])
        p = strings.Replace(p, s, r.where[v[1]], -1)
    }
    regex, err := regexp.Compile(p)
    if err != nil {
        return false
    }
    matches = regex.FindAllStringSubmatch(req.URL.Path, -1)
    for _, match := range matches {

        if regex.Match([]byte(match[0])) {
            return true
        }
    }
    return false
}

func (r *Route) clear(s string) string {
    s = strings.Replace(s, "{", "", -1)
    s = strings.Replace(s, "}", "", -1)
    return s
}

// Where define a regex pattern for the variables in the route path
func (r *Route) Where(key string, pattern string) *Route {
    r.where[key] = fmt.Sprintf("(%s)", pattern)
    return r
}

// Middleware register a collection of middleware functions and sort them
func (r *Route) Middleware(mw ...Middleware) *Route {
    r.mw = mw

    //TODO: Fix this
    for i := len(r.mw)/2 - 1; i >= 0; i-- {
        opp := len(r.mw) - 1 - i
        r.mw[i], r.mw[opp] = r.mw[opp], r.mw[i]
    }
    return r
}

// extractVars parse the requested URL and return key/value pair of
// variables defined in the route path.
func (r *Route) extractVars(req *http.Request) map[string]string {
    url := strings.Split(req.URL.Path, "/")
    path := strings.Split(r.clear(r.path), "/")
    vars := make(map[string]string)
    for i := 0; i < len(url); i++ {
        if _, ok := r.where[path[i]]; ok {
            vars[path[i]] = url[i]
        }
    }
    return vars
}

// dispatch run route middleswares if any then run the route handler
func (r *Route) dispatch() http.Handler {
    for _, m := range r.mw {
        r.handler = m(r.handler)
    }
    return r.handler
}

How to use it:

func main() {
  r := router.NewRouter()

  r.GET("/user/{user}", userHandler).
      Name("index").
      Where("user", "[a-z0-9]+").
      Middleware(mid1)

  http.ListenAndServe(":3000", r)
}

func userHandler(w http.ResponseWriter, r *http.Request) {
   vars := router.Vars(r)
   fmt.Fprintf(w, "hello, %s!", vars["user"])
}

func mid1(next http.Handler) http.Handler {
  return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request){
      fmt.Println("from middleware 1")
      next.ServeHTTP(w, r) // call another middleware or the final handler
  });
}
\$\endgroup\$
1
\$\begingroup\$

You write:

func (r *Route) Match(req *http.Request) bool {
    regex := regexp.MustCompile(`{([^}]*)}`)
    // ...
}

Therefore, we would expect your performance to be poor.

See https://codereview.stackexchange.com/a/236196/13970

What performance testing have you done? Where are your benchmarks?

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks for the reply. I haven't done any benchmark but I think the regex in Go is slow and it's not specific to MustCompile function. right? Do you have any suggestion to fix it? \$\endgroup\$ – Saeed M. Mar 3 at 3:57
  • \$\begingroup\$ @SaeedM.: In my answer I gave you a link to an earlier answer which explained the problem and how to solve it by moving a line. \$\endgroup\$ – peterSO Mar 4 at 0:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.