0
\$\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
 });
}
asked Mar 2, 2020 at 13:07
\$\endgroup\$

1 Answer 1

2
\$\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?

answered Mar 2, 2020 at 13:53
\$\endgroup\$
2
  • \$\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\$ Commented Mar 3, 2020 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\$ Commented Mar 4, 2020 at 0:57

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.