diff --git a/README.md b/README.md index 63b6eb8f..6e42d1b1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ The router is optimized for high performance and a small memory footprint. It sc **Parameters in your routing pattern:** Stop parsing the requested URL path, just give the path segment a name and the router delivers the dynamic value to you. Because of the design of the router, path parameters are very cheap. +**RouteGroups:** A way to create groups of routes without incuring any per-request overhead. + **Zero Garbage:** The matching and dispatching process generates zero bytes of garbage. The only heap allocations that are made are building the slice of the key-value pairs for path parameters, and building new context and request objects (the latter only in the standard `Handler`/`HandlerFunc` API). In the 3-argument API, if the request path contains no parameters not a single heap allocation is necessary. **Best Performance:** [Benchmarks speak for themselves](https://github.com/julienschmidt/go-http-routing-benchmark). See below for technical details of the implementation. diff --git a/routegroup.go b/routegroup.go new file mode 100644 index 00000000..3894bc58 --- /dev/null +++ b/routegroup.go @@ -0,0 +1,67 @@ +package httprouter + +import ( + "net/http" +) + +type RouteGroup struct { + r *Router + p string +} + +func newRouteGroup(r *Router, path string) *RouteGroup { + if path[0] != '/' { + panic("path must begin with '/' in path '" + path + "'") + } + + //Strip traling / (if present) as all added sub paths must start with a / + if path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + return &RouteGroup{r: r, p: path} +} + +func (r *RouteGroup) NewGroup(path string) *RouteGroup { + return newRouteGroup(r.r, r.subPath(path)) +} + +func (r *RouteGroup) Handle(method, path string, handle Handle) { + r.r.Handle(method, r.subPath(path), handle) +} + +func (r *RouteGroup) Handler(method, path string, handler http.Handler) { + r.r.Handler(method, r.subPath(path), handler) +} + +func (r *RouteGroup) HandlerFunc(method, path string, handler http.HandlerFunc) { + r.r.HandlerFunc(method, r.subPath(path), handler) +} + +func (r *RouteGroup) GET(path string, handle Handle) { + r.Handle("GET", path, handle) +} +func (r *RouteGroup) HEAD(path string, handle Handle) { + r.Handle("HEAD", path, handle) +} +func (r *RouteGroup) OPTIONS(path string, handle Handle) { + r.Handle("OPTIONS", path, handle) +} +func (r *RouteGroup) POST(path string, handle Handle) { + r.Handle("POST", path, handle) +} +func (r *RouteGroup) PUT(path string, handle Handle) { + r.Handle("PUT", path, handle) +} +func (r *RouteGroup) PATCH(path string, handle Handle) { + r.Handle("PATCH", path, handle) +} +func (r *RouteGroup) DELETE(path string, handle Handle) { + r.Handle("DELETE", path, handle) +} + +func (r *RouteGroup) subPath(path string) string { + if path[0] != '/' { + panic("path must start with a '/'") + } + return r.p + path +} diff --git a/routegroup_test.go b/routegroup_test.go new file mode 100644 index 00000000..d2ed3251 --- /dev/null +++ b/routegroup_test.go @@ -0,0 +1,117 @@ +package httprouter + +import ( + "net/http" + "testing" +) + +func TestRouteGroupOfARouteGroup(t *testing.T) { + var get bool + router := New() + foo := router.NewGroup("/foo") // creates /foo group + bar := foo.NewGroup("/bar") + + bar.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { + get = true + }) + + w := new(mockResponseWriter) + + r, _ := http.NewRequest("GET", "/foo/bar/GET", nil) + router.ServeHTTP(w, r) + if !get { + t.Error("routing GET /foo/bar/GET failed") + } + +} + +func TestRouteGroupAPI(t *testing.T) { + var get, head, options, post, put, patch, delete, handler, handlerFunc bool + + httpHandler := handlerStruct{&handler} + + router := New() + group := router.NewGroup("/foo") // creates /foo group + + group.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { + get = true + }) + group.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { + head = true + }) + group.OPTIONS("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { + options = true + }) + group.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) { + post = true + }) + group.PUT("/PUT", func(w http.ResponseWriter, r *http.Request, _ Params) { + put = true + }) + group.PATCH("/PATCH", func(w http.ResponseWriter, r *http.Request, _ Params) { + patch = true + }) + group.DELETE("/DELETE", func(w http.ResponseWriter, r *http.Request, _ Params) { + delete = true + }) + group.Handler("GET", "/Handler", httpHandler) + group.HandlerFunc("GET", "/HandlerFunc", func(w http.ResponseWriter, r *http.Request) { + handlerFunc = true + }) + + w := new(mockResponseWriter) + + r, _ := http.NewRequest("GET", "/foo/GET", nil) + router.ServeHTTP(w, r) + if !get { + t.Error("routing /foo/GET failed") + } + + r, _ = http.NewRequest("HEAD", "/foo/GET", nil) + router.ServeHTTP(w, r) + if !head { + t.Error("routing HEAD failed") + } + + r, _ = http.NewRequest("OPTIONS", "/foo/GET", nil) + router.ServeHTTP(w, r) + if !options { + t.Error("routing OPTIONS failed") + } + + r, _ = http.NewRequest("POST", "/foo/POST", nil) + router.ServeHTTP(w, r) + if !post { + t.Error("routing POST failed") + } + + r, _ = http.NewRequest("PUT", "/foo/PUT", nil) + router.ServeHTTP(w, r) + if !put { + t.Error("routing PUT failed") + } + + r, _ = http.NewRequest("PATCH", "/foo/PATCH", nil) + router.ServeHTTP(w, r) + if !patch { + t.Error("routing PATCH failed") + } + + r, _ = http.NewRequest("DELETE", "/foo/DELETE", nil) + router.ServeHTTP(w, r) + if !delete { + t.Error("routing DELETE failed") + } + + r, _ = http.NewRequest("GET", "/foo/Handler", nil) + router.ServeHTTP(w, r) + if !handler { + t.Error("routing Handler failed") + } + + r, _ = http.NewRequest("GET", "/foo/HandlerFunc", nil) + router.ServeHTTP(w, r) + if !handlerFunc { + t.Error("routing HandlerFunc failed") + } +} diff --git a/router.go b/router.go index 1eab403d..10e6ef7e 100644 --- a/router.go +++ b/router.go @@ -6,30 +6,30 @@ // // A trivial example is: // -// package main +// package main // -// import ( -// "fmt" -// "github.com/julienschmidt/httprouter" -// "net/http" -// "log" -// ) +// import ( +// "fmt" +// "github.com/julienschmidt/httprouter" +// "net/http" +// "log" +// ) // -// func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { -// fmt.Fprint(w, "Welcome!\n") -// } +// func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +// fmt.Fprint(w, "Welcome!\n") +// } // -// func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { -// fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) -// } +// func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { +// fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) +// } // -// func main() { -// router := httprouter.New() -// router.GET("/", Index) -// router.GET("/hello/:name", Hello) +// func main() { +// router := httprouter.New() +// router.GET("/", Index) +// router.GET("/hello/:name", Hello) // -// log.Fatal(http.ListenAndServe(":8080", router)) -// } +// log.Fatal(http.ListenAndServe(":8080", router)) +// } // // The router matches incoming requests by the request method and the path. // If a handle is registered for this path and method, the router delegates the @@ -39,41 +39,45 @@ // // The registered path, against which the router matches incoming requests, can // contain two types of parameters: -// Syntax Type -// :name named parameter -// *name catch-all parameter +// +// Syntax Type +// :name named parameter +// *name catch-all parameter // // Named parameters are dynamic path segments. They match anything until the // next '/' or the path end: -// Path: /blog/:category/:post // -// Requests: -// /blog/go/request-routers match: category="go", post="request-routers" -// /blog/go/request-routers/ no match, but the router would redirect -// /blog/go/ no match -// /blog/go/request-routers/comments no match +// Path: /blog/:category/:post +// +// Requests: +// /blog/go/request-routers match: category="go", post="request-routers" +// /blog/go/request-routers/ no match, but the router would redirect +// /blog/go/ no match +// /blog/go/request-routers/comments no match // // Catch-all parameters match anything until the path end, including the // directory index (the '/' before the catch-all). Since they match anything // until the end, catch-all parameters must always be the final path element. -// Path: /files/*filepath // -// Requests: -// /files/ match: filepath="/" -// /files/LICENSE match: filepath="/LICENSE" -// /files/templates/article.html match: filepath="/templates/article.html" -// /files no match, but the router would redirect +// Path: /files/*filepath +// +// Requests: +// /files/ match: filepath="/" +// /files/LICENSE match: filepath="/LICENSE" +// /files/templates/article.html match: filepath="/templates/article.html" +// /files no match, but the router would redirect // // The value of parameters is saved as a slice of the Param struct, consisting // each of a key and a value. The slice is passed to the Handle func as a third // parameter. // There are two ways to retrieve the value of a parameter: -// // by the name of the parameter -// user := ps.ByName("user") // defined by :user or *user // -// // by the index of the parameter. This way you can also get the name (key) -// thirdKey := ps[2].Key // the name of the 3rd parameter -// thirdValue := ps[2].Value // the value of the 3rd parameter +// // by the name of the parameter +// user := ps.ByName("user") // defined by :user or *user +// +// // by the index of the parameter. This way you can also get the name (key) +// thirdKey := ps[2].Key // the name of the 3rd parameter +// thirdValue := ps[2].Value // the value of the 3rd parameter package httprouter import ( @@ -246,7 +250,12 @@ func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle { } } -// GET is a shortcut for router.Handle(http.MethodGet, path, handle) +// NewGroup adds a zero overhead group of routes that share a common root path. +func (r *Router) NewGroup(path string) *RouteGroup { + return newRouteGroup(r, path) +} + +// GET is a shortcut for router.Handle("GET", path, handle) func (r *Router) GET(path string, handle Handle) { r.Handle(http.MethodGet, path, handle) } @@ -366,7 +375,8 @@ func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { // of the Router's NotFound handler. // To use the operating system's file system implementation, // use http.Dir: -// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) +// +// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) func (r *Router) ServeFiles(path string, root http.FileSystem) { if len(path) < 10 || path[len(path)-10:] != "/*filepath" { panic("path must end with /*filepath in path '" + path + "'")