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
});
}
1 Answer 1
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?
-
\$\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.– Saeed M.2020年03月03日 03:57:01 +00:00Commented 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\$peterSO– peterSO2020年03月04日 00:57:08 +00:00Commented Mar 4, 2020 at 0:57