Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subrouter / RouteGroups #89

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
67 changes: 67 additions & 0 deletions routegroup.go
Original file line number Diff line number Diff line change
@@ -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
}
117 changes: 117 additions & 0 deletions routegroup_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
92 changes: 51 additions & 41 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 + "'")
Expand Down