Skip to content

Commit 1d4a653

Browse files
dbudworthDavid Budworth
authored andcommitted
Introduction of RouteGroup concept
RouteGroup is a simple route mapper that delegates to an underlying Router instance when mapping paths and takes no part in handling of requests resulting in zero added per-request cost. Implemented as a simple struct to keep in the spirit of httprouter's design. Potential improvement being to create an interface for the common functions of Router and RouteGroup.
1 parent 3425025 commit 1d4a653

4 files changed

Lines changed: 237 additions & 41 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ The router is optimized for high performance and a small memory footprint. It sc
1616

1717
**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.
1818

19+
**RouteGroups:** A way to create groups of routes without incuring any per-request overhead.
20+
1921
**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.
2022

2123
**Best Performance:** [Benchmarks speak for themselves](https://github.com/julienschmidt/go-http-routing-benchmark). See below for technical details of the implementation.

routegroup.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package httprouter
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
type RouteGroup struct {
8+
r *Router
9+
p string
10+
}
11+
12+
func newRouteGroup(r *Router, path string) *RouteGroup {
13+
if path[0] != '/' {
14+
panic("path must begin with '/' in path '" + path + "'")
15+
}
16+
17+
//Strip traling / (if present) as all added sub paths must start with a /
18+
if path[len(path)-1] == '/' {
19+
path = path[:len(path)-1]
20+
}
21+
return &RouteGroup{r: r, p: path}
22+
}
23+
24+
func (r *RouteGroup) NewGroup(path string) *RouteGroup {
25+
return newRouteGroup(r.r, r.subPath(path))
26+
}
27+
28+
func (r *RouteGroup) Handle(method, path string, handle Handle) {
29+
r.r.Handle(method, r.subPath(path), handle)
30+
}
31+
32+
func (r *RouteGroup) Handler(method, path string, handler http.Handler) {
33+
r.r.Handler(method, r.subPath(path), handler)
34+
}
35+
36+
func (r *RouteGroup) HandlerFunc(method, path string, handler http.HandlerFunc) {
37+
r.r.HandlerFunc(method, r.subPath(path), handler)
38+
}
39+
40+
func (r *RouteGroup) GET(path string, handle Handle) {
41+
r.Handle("GET", path, handle)
42+
}
43+
func (r *RouteGroup) HEAD(path string, handle Handle) {
44+
r.Handle("HEAD", path, handle)
45+
}
46+
func (r *RouteGroup) OPTIONS(path string, handle Handle) {
47+
r.Handle("OPTIONS", path, handle)
48+
}
49+
func (r *RouteGroup) POST(path string, handle Handle) {
50+
r.Handle("POST", path, handle)
51+
}
52+
func (r *RouteGroup) PUT(path string, handle Handle) {
53+
r.Handle("PUT", path, handle)
54+
}
55+
func (r *RouteGroup) PATCH(path string, handle Handle) {
56+
r.Handle("PATCH", path, handle)
57+
}
58+
func (r *RouteGroup) DELETE(path string, handle Handle) {
59+
r.Handle("DELETE", path, handle)
60+
}
61+
62+
func (r *RouteGroup) subPath(path string) string {
63+
if path[0] != '/' {
64+
panic("path must start with a '/'")
65+
}
66+
return r.p + path
67+
}

routegroup_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package httprouter
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
)
7+
8+
func TestRouteGroupOfARouteGroup(t *testing.T) {
9+
var get bool
10+
router := New()
11+
foo := router.NewGroup("/foo") // creates /foo group
12+
bar := foo.NewGroup("/bar")
13+
14+
bar.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
15+
get = true
16+
})
17+
18+
w := new(mockResponseWriter)
19+
20+
r, _ := http.NewRequest("GET", "/foo/bar/GET", nil)
21+
router.ServeHTTP(w, r)
22+
if !get {
23+
t.Error("routing GET /foo/bar/GET failed")
24+
}
25+
26+
}
27+
28+
func TestRouteGroupAPI(t *testing.T) {
29+
var get, head, options, post, put, patch, delete, handler, handlerFunc bool
30+
31+
httpHandler := handlerStruct{&handler}
32+
33+
router := New()
34+
group := router.NewGroup("/foo") // creates /foo group
35+
36+
group.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
37+
get = true
38+
})
39+
group.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
40+
head = true
41+
})
42+
group.OPTIONS("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
43+
options = true
44+
})
45+
group.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) {
46+
post = true
47+
})
48+
group.PUT("/PUT", func(w http.ResponseWriter, r *http.Request, _ Params) {
49+
put = true
50+
})
51+
group.PATCH("/PATCH", func(w http.ResponseWriter, r *http.Request, _ Params) {
52+
patch = true
53+
})
54+
group.DELETE("/DELETE", func(w http.ResponseWriter, r *http.Request, _ Params) {
55+
delete = true
56+
})
57+
group.Handler("GET", "/Handler", httpHandler)
58+
group.HandlerFunc("GET", "/HandlerFunc", func(w http.ResponseWriter, r *http.Request) {
59+
handlerFunc = true
60+
})
61+
62+
w := new(mockResponseWriter)
63+
64+
r, _ := http.NewRequest("GET", "/foo/GET", nil)
65+
router.ServeHTTP(w, r)
66+
if !get {
67+
t.Error("routing /foo/GET failed")
68+
}
69+
70+
r, _ = http.NewRequest("HEAD", "/foo/GET", nil)
71+
router.ServeHTTP(w, r)
72+
if !head {
73+
t.Error("routing HEAD failed")
74+
}
75+
76+
r, _ = http.NewRequest("OPTIONS", "/foo/GET", nil)
77+
router.ServeHTTP(w, r)
78+
if !options {
79+
t.Error("routing OPTIONS failed")
80+
}
81+
82+
r, _ = http.NewRequest("POST", "/foo/POST", nil)
83+
router.ServeHTTP(w, r)
84+
if !post {
85+
t.Error("routing POST failed")
86+
}
87+
88+
r, _ = http.NewRequest("PUT", "/foo/PUT", nil)
89+
router.ServeHTTP(w, r)
90+
if !put {
91+
t.Error("routing PUT failed")
92+
}
93+
94+
r, _ = http.NewRequest("PATCH", "/foo/PATCH", nil)
95+
router.ServeHTTP(w, r)
96+
if !patch {
97+
t.Error("routing PATCH failed")
98+
}
99+
100+
r, _ = http.NewRequest("DELETE", "/foo/DELETE", nil)
101+
router.ServeHTTP(w, r)
102+
if !delete {
103+
t.Error("routing DELETE failed")
104+
}
105+
106+
r, _ = http.NewRequest("GET", "/foo/Handler", nil)
107+
router.ServeHTTP(w, r)
108+
if !handler {
109+
t.Error("routing Handler failed")
110+
}
111+
112+
r, _ = http.NewRequest("GET", "/foo/HandlerFunc", nil)
113+
router.ServeHTTP(w, r)
114+
if !handlerFunc {
115+
t.Error("routing HandlerFunc failed")
116+
}
117+
}

router.go

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,30 @@
66
//
77
// A trivial example is:
88
//
9-
// package main
9+
// package main
1010
//
11-
// import (
12-
// "fmt"
13-
// "github.com/julienschmidt/httprouter"
14-
// "net/http"
15-
// "log"
16-
// )
11+
// import (
12+
// "fmt"
13+
// "github.com/julienschmidt/httprouter"
14+
// "net/http"
15+
// "log"
16+
// )
1717
//
18-
// func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
19-
// fmt.Fprint(w, "Welcome!\n")
20-
// }
18+
// func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
19+
// fmt.Fprint(w, "Welcome!\n")
20+
// }
2121
//
22-
// func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
23-
// fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
24-
// }
22+
// func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
23+
// fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
24+
// }
2525
//
26-
// func main() {
27-
// router := httprouter.New()
28-
// router.GET("/", Index)
29-
// router.GET("/hello/:name", Hello)
26+
// func main() {
27+
// router := httprouter.New()
28+
// router.GET("/", Index)
29+
// router.GET("/hello/:name", Hello)
3030
//
31-
// log.Fatal(http.ListenAndServe(":8080", router))
32-
// }
31+
// log.Fatal(http.ListenAndServe(":8080", router))
32+
// }
3333
//
3434
// The router matches incoming requests by the request method and the path.
3535
// If a handle is registered for this path and method, the router delegates the
@@ -39,41 +39,45 @@
3939
//
4040
// The registered path, against which the router matches incoming requests, can
4141
// contain two types of parameters:
42-
// Syntax Type
43-
// :name named parameter
44-
// *name catch-all parameter
42+
//
43+
// Syntax Type
44+
// :name named parameter
45+
// *name catch-all parameter
4546
//
4647
// Named parameters are dynamic path segments. They match anything until the
4748
// next '/' or the path end:
48-
// Path: /blog/:category/:post
4949
//
50-
// Requests:
51-
// /blog/go/request-routers match: category="go", post="request-routers"
52-
// /blog/go/request-routers/ no match, but the router would redirect
53-
// /blog/go/ no match
54-
// /blog/go/request-routers/comments no match
50+
// Path: /blog/:category/:post
51+
//
52+
// Requests:
53+
// /blog/go/request-routers match: category="go", post="request-routers"
54+
// /blog/go/request-routers/ no match, but the router would redirect
55+
// /blog/go/ no match
56+
// /blog/go/request-routers/comments no match
5557
//
5658
// Catch-all parameters match anything until the path end, including the
5759
// directory index (the '/' before the catch-all). Since they match anything
5860
// until the end, catch-all parameters must always be the final path element.
59-
// Path: /files/*filepath
6061
//
61-
// Requests:
62-
// /files/ match: filepath="/"
63-
// /files/LICENSE match: filepath="/LICENSE"
64-
// /files/templates/article.html match: filepath="/templates/article.html"
65-
// /files no match, but the router would redirect
62+
// Path: /files/*filepath
63+
//
64+
// Requests:
65+
// /files/ match: filepath="/"
66+
// /files/LICENSE match: filepath="/LICENSE"
67+
// /files/templates/article.html match: filepath="/templates/article.html"
68+
// /files no match, but the router would redirect
6669
//
6770
// The value of parameters is saved as a slice of the Param struct, consisting
6871
// each of a key and a value. The slice is passed to the Handle func as a third
6972
// parameter.
7073
// There are two ways to retrieve the value of a parameter:
71-
// // by the name of the parameter
72-
// user := ps.ByName("user") // defined by :user or *user
7374
//
74-
// // by the index of the parameter. This way you can also get the name (key)
75-
// thirdKey := ps[2].Key // the name of the 3rd parameter
76-
// thirdValue := ps[2].Value // the value of the 3rd parameter
75+
// // by the name of the parameter
76+
// user := ps.ByName("user") // defined by :user or *user
77+
//
78+
// // by the index of the parameter. This way you can also get the name (key)
79+
// thirdKey := ps[2].Key // the name of the 3rd parameter
80+
// thirdValue := ps[2].Value // the value of the 3rd parameter
7781
package httprouter
7882

7983
import (
@@ -246,7 +250,12 @@ func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle {
246250
}
247251
}
248252

249-
// GET is a shortcut for router.Handle(http.MethodGet, path, handle)
253+
// NewGroup adds a zero overhead group of routes that share a common root path.
254+
func (r *Router) NewGroup(path string) *RouteGroup {
255+
return newRouteGroup(r, path)
256+
}
257+
258+
// GET is a shortcut for router.Handle("GET", path, handle)
250259
func (r *Router) GET(path string, handle Handle) {
251260
r.Handle(http.MethodGet, path, handle)
252261
}
@@ -366,7 +375,8 @@ func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
366375
// of the Router's NotFound handler.
367376
// To use the operating system's file system implementation,
368377
// use http.Dir:
369-
// router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
378+
//
379+
// router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
370380
func (r *Router) ServeFiles(path string, root http.FileSystem) {
371381
if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
372382
panic("path must end with /*filepath in path '" + path + "'")

0 commit comments

Comments
 (0)