Ver código fonte

Zero allocation router, first commit

Manu Mtz-Almeida 10 anos atrás
pai
commit
2915fa0ffe
8 arquivos alterados com 909 adições e 172 exclusões
  1. 1 2
      context.go
  2. 0 132
      deprecated.go
  3. 101 29
      gin.go
  4. 123 0
      path.go
  5. 92 0
      path_test.go
  6. 1 9
      routergroup.go
  7. 556 0
      tree.go
  8. 35 0
      utils.go

+ 1 - 2
context.go

@@ -13,7 +13,6 @@ import (
 
 	"github.com/gin-gonic/gin/binding"
 	"github.com/gin-gonic/gin/render"
-	"github.com/julienschmidt/httprouter"
 )
 
 const AbortIndex = math.MaxInt8 / 2
@@ -26,7 +25,7 @@ type Context struct {
 	Request   *http.Request
 	Writer    ResponseWriter
 
-	Params   httprouter.Params
+	Params   Params
 	Input    inputHolder
 	handlers []HandlerFunc
 	index    int8

+ 0 - 132
deprecated.go

@@ -3,135 +3,3 @@
 // license that can be found in the LICENSE file.
 
 package gin
-
-import (
-	"log"
-	"net"
-	"net/http"
-	"strings"
-
-	"github.com/gin-gonic/gin/binding"
-)
-
-const (
-	MIMEJSON              = binding.MIMEJSON
-	MIMEHTML              = binding.MIMEHTML
-	MIMEXML               = binding.MIMEXML
-	MIMEXML2              = binding.MIMEXML2
-	MIMEPlain             = binding.MIMEPlain
-	MIMEPOSTForm          = binding.MIMEPOSTForm
-	MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
-)
-
-// DEPRECATED, use Bind() instead.
-// Like ParseBody() but this method also writes a 400 error if the json is not valid.
-func (c *Context) EnsureBody(item interface{}) bool {
-	return c.Bind(item)
-}
-
-// DEPRECATED use bindings directly
-// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer.
-func (c *Context) ParseBody(item interface{}) error {
-	return binding.JSON.Bind(c.Request, item)
-}
-
-// DEPRECATED use gin.Static() instead
-// ServeFiles serves files from the given file system root.
-// The path must end with "/*filepath", files are then served from the local
-// path /defined/root/dir/*filepath.
-// For example if root is "/etc" and *filepath is "passwd", the local file
-// "/etc/passwd" would be served.
-// Internally a http.FileServer is used, therefore http.NotFound is used instead
-// 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"))
-func (engine *Engine) ServeFiles(path string, root http.FileSystem) {
-	engine.router.ServeFiles(path, root)
-}
-
-// DEPRECATED use gin.LoadHTMLGlob() or gin.LoadHTMLFiles() instead
-func (engine *Engine) LoadHTMLTemplates(pattern string) {
-	engine.LoadHTMLGlob(pattern)
-}
-
-// DEPRECATED. Use NoRoute() instead
-func (engine *Engine) NotFound404(handlers ...HandlerFunc) {
-	engine.NoRoute(handlers...)
-}
-
-// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this
-// middleware if you've got servers in front of this server. The list with (known) proxies and
-// local ips are being filtered out of the forwarded for list, giving the last not local ip being
-// the real client ip.
-func ForwardedFor(proxies ...interface{}) HandlerFunc {
-	if len(proxies) == 0 {
-		// default to local ips
-		var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"}
-
-		proxies = make([]interface{}, len(reservedLocalIps))
-
-		for i, v := range reservedLocalIps {
-			proxies[i] = v
-		}
-	}
-
-	return func(c *Context) {
-		// the X-Forwarded-For header contains an array with left most the client ip, then
-		// comma separated, all proxies the request passed. The last proxy appears
-		// as the remote address of the request. Returning the client
-		// ip to comply with default RemoteAddr response.
-
-		// check if remoteaddr is local ip or in list of defined proxies
-		remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0])
-
-		if !ipInMasks(remoteIp, proxies) {
-			return
-		}
-
-		if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" {
-			parts := strings.Split(forwardedFor, ",")
-
-			for i := len(parts) - 1; i >= 0; i-- {
-				part := parts[i]
-
-				ip := net.ParseIP(strings.TrimSpace(part))
-
-				if ipInMasks(ip, proxies) {
-					continue
-				}
-
-				// returning remote addr conform the original remote addr format
-				c.Request.RemoteAddr = ip.String() + ":0"
-
-				// remove forwarded for address
-				c.Request.Header.Set("X-Forwarded-For", "")
-				return
-			}
-		}
-	}
-}
-
-func ipInMasks(ip net.IP, masks []interface{}) bool {
-	for _, proxy := range masks {
-		var mask *net.IPNet
-		var err error
-
-		switch t := proxy.(type) {
-		case string:
-			if _, mask, err = net.ParseCIDR(t); err != nil {
-				log.Panic(err)
-			}
-		case net.IP:
-			mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)}
-		case net.IPNet:
-			mask = &t
-		}
-
-		if mask.Contains(ip) {
-			return true
-		}
-	}
-
-	return false
-}

+ 101 - 29
gin.go

@@ -11,9 +11,32 @@ import (
 
 	"github.com/gin-gonic/gin/binding"
 	"github.com/gin-gonic/gin/render"
-	"github.com/julienschmidt/httprouter"
 )
 
+// Param is a single URL parameter, consisting of a key and a value.
+type Param struct {
+	Key   string
+	Value string
+}
+
+// Params is a Param-slice, as returned by the router.
+// The slice is ordered, the first URL parameter is also the first slice value.
+// It is therefore safe to read values by the index.
+type Params []Param
+
+// ByName returns the value of the first Param which key matches the given name.
+// If no matching Param is found, an empty string is returned.
+func (ps Params) ByName(name string) string {
+	for i := range ps {
+		if ps[i].Key == name {
+			return ps[i].Value
+		}
+	}
+	return ""
+}
+
+var default404Body = []byte("404 page not found")
+var default405Body = []byte("405 method not allowed")
 
 type (
 	HandlerFunc func(*Context)
@@ -21,32 +44,56 @@ type (
 	// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares.
 	Engine struct {
 		*RouterGroup
-		HTMLRender     render.Render
-		Default404Body []byte
-		Default405Body []byte
-		pool           sync.Pool
-		allNoRoute     []HandlerFunc
-		allNoMethod    []HandlerFunc
-		noRoute        []HandlerFunc
-		noMethod       []HandlerFunc
-		router         *httprouter.Router
+		HTMLRender  render.Render
+		pool        sync.Pool
+		allNoRoute  []HandlerFunc
+		allNoMethod []HandlerFunc
+		noRoute     []HandlerFunc
+		noMethod    []HandlerFunc
+		trees       map[string]*node
+
+		// Enables automatic redirection if the current route can't be matched but a
+		// handler for the path with (without) the trailing slash exists.
+		// For example if /foo/ is requested but a route only exists for /foo, the
+		// client is redirected to /foo with http status code 301 for GET requests
+		// and 307 for all other request methods.
+		RedirectTrailingSlash bool
+
+		// If enabled, the router tries to fix the current request path, if no
+		// handle is registered for it.
+		// First superfluous path elements like ../ or // are removed.
+		// Afterwards the router does a case-insensitive lookup of the cleaned path.
+		// If a handle can be found for this route, the router makes a redirection
+		// to the corrected path with status code 301 for GET requests and 307 for
+		// all other request methods.
+		// For example /FOO and /..//Foo could be redirected to /foo.
+		// RedirectTrailingSlash is independent of this option.
+		RedirectFixedPath bool
+
+		// If enabled, the router checks if another method is allowed for the
+		// current route, if the current request can not be routed.
+		// If this is the case, the request is answered with 'Method Not Allowed'
+		// and HTTP status code 405.
+		// If no other Method is allowed, the request is delegated to the NotFound
+		// handler.
+		HandleMethodNotAllowed bool
 	}
 )
 
 // Returns a new blank Engine instance without any middleware attached.
 // The most basic configuration
 func New() *Engine {
-	engine := &Engine{}
+	engine := &Engine{
+		RedirectTrailingSlash:  true,
+		RedirectFixedPath:      true,
+		HandleMethodNotAllowed: true,
+		trees: make(map[string]*node),
+	}
 	engine.RouterGroup = &RouterGroup{
 		Handlers:     nil,
 		absolutePath: "/",
 		engine:       engine,
 	}
-	engine.router = httprouter.New()
-	engine.Default404Body = []byte("404 page not found")
-	engine.Default405Body = []byte("405 method not allowed")
-	engine.router.NotFound = engine.handle404
-	engine.router.MethodNotAllowed = engine.handle405
 	engine.pool.New = func() interface{} {
 		return engine.allocateContext()
 	}
@@ -67,13 +114,11 @@ func (engine *Engine) allocateContext() (context *Context) {
 	return
 }
 
-func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context {
+func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request) *Context {
 	c := engine.pool.Get().(*Context)
 	c.reset()
 	c.writermem.reset(w)
 	c.Request = req
-	c.Params = params
-	c.handlers = handlers
 	return c
 }
 
@@ -132,39 +177,66 @@ func (engine *Engine) rebuild405Handlers() {
 	engine.allNoMethod = engine.combineHandlers(engine.noMethod)
 }
 
-func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
-	c := engine.createContext(w, req, nil, engine.allNoRoute)
+func (engine *Engine) handle404(c *Context) {
 	// set 404 by default, useful for logging
+	c.handlers = engine.allNoRoute
 	c.Writer.WriteHeader(404)
 	c.Next()
 	if !c.Writer.Written() {
 		if c.Writer.Status() == 404 {
-			c.Data(-1, binding.MIMEPlain, engine.Default404Body)
+			c.Data(-1, binding.MIMEPlain, default404Body)
 		} else {
 			c.Writer.WriteHeaderNow()
 		}
 	}
-	engine.reuseContext(c)
 }
 
-func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) {
-	c := engine.createContext(w, req, nil, engine.allNoMethod)
+func (engine *Engine) handle405(c *Context) {
 	// set 405 by default, useful for logging
+	c.handlers = engine.allNoMethod
 	c.Writer.WriteHeader(405)
 	c.Next()
 	if !c.Writer.Written() {
 		if c.Writer.Status() == 405 {
-			c.Data(-1, binding.MIMEPlain, engine.Default405Body)
+			c.Data(-1, binding.MIMEPlain, default405Body)
 		} else {
 			c.Writer.WriteHeaderNow()
 		}
 	}
-	engine.reuseContext(c)
+}
+
+func (engine *Engine) handle(method, path string, handlers []HandlerFunc) {
+	if path[0] != '/' {
+		panic("path must begin with '/'")
+	}
+
+	//methodCode := codeForHTTPMethod(method)
+	root := engine.trees[method]
+	if root == nil {
+		root = new(node)
+		engine.trees[method] = root
+	}
+	root.addRoute(path, handlers)
 }
 
 // ServeHTTP makes the router implement the http.Handler interface.
-func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
-	engine.router.ServeHTTP(writer, request)
+func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	c := engine.createContext(w, req)
+	//methodCode := codeForHTTPMethod(req.Method)
+	if root := engine.trees[req.Method]; root != nil {
+		path := req.URL.Path
+		if handlers, params, _ := root.getValue(path, c.Params); handlers != nil {
+			c.handlers = handlers
+			c.Params = params
+			c.Next()
+			engine.reuseContext(c)
+			return
+		}
+	}
+
+	// Handle 404
+	engine.handle404(c)
+	engine.reuseContext(c)
 }
 
 func (engine *Engine) Run(addr string) error {

+ 123 - 0
path.go

@@ -0,0 +1,123 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Based on the path package, Copyright 2009 The Go Authors.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package gin
+
+// CleanPath is the URL version of path.Clean, it returns a canonical URL path
+// for p, eliminating . and .. elements.
+//
+// The following rules are applied iteratively until no further processing can
+// be done:
+//	1. Replace multiple slashes with a single slash.
+//	2. Eliminate each . path name element (the current directory).
+//	3. Eliminate each inner .. path name element (the parent directory)
+//	   along with the non-.. element that precedes it.
+//	4. Eliminate .. elements that begin a rooted path:
+//	   that is, replace "/.." by "/" at the beginning of a path.
+//
+// If the result of this process is an empty string, "/" is returned
+func CleanPath(p string) string {
+	// Turn empty string into "/"
+	if p == "" {
+		return "/"
+	}
+
+	n := len(p)
+	var buf []byte
+
+	// Invariants:
+	//      reading from path; r is index of next byte to process.
+	//      writing to buf; w is index of next byte to write.
+
+	// path must start with '/'
+	r := 1
+	w := 1
+
+	if p[0] != '/' {
+		r = 0
+		buf = make([]byte, n+1)
+		buf[0] = '/'
+	}
+
+	trailing := n > 2 && p[n-1] == '/'
+
+	// A bit more clunky without a 'lazybuf' like the path package, but the loop
+	// gets completely inlined (bufApp). So in contrast to the path package this
+	// loop has no expensive function calls (except 1x make)
+
+	for r < n {
+		switch {
+		case p[r] == '/':
+			// empty path element, trailing slash is added after the end
+			r++
+
+		case p[r] == '.' && r+1 == n:
+			trailing = true
+			r++
+
+		case p[r] == '.' && p[r+1] == '/':
+			// . element
+			r++
+
+		case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
+			// .. element: remove to last /
+			r += 2
+
+			if w > 1 {
+				// can backtrack
+				w--
+
+				if buf == nil {
+					for w > 1 && p[w] != '/' {
+						w--
+					}
+				} else {
+					for w > 1 && buf[w] != '/' {
+						w--
+					}
+				}
+			}
+
+		default:
+			// real path element.
+			// add slash if needed
+			if w > 1 {
+				bufApp(&buf, p, w, '/')
+				w++
+			}
+
+			// copy element
+			for r < n && p[r] != '/' {
+				bufApp(&buf, p, w, p[r])
+				w++
+				r++
+			}
+		}
+	}
+
+	// re-append trailing slash
+	if trailing && w > 1 {
+		bufApp(&buf, p, w, '/')
+		w++
+	}
+
+	if buf == nil {
+		return p[:w]
+	}
+	return string(buf[:w])
+}
+
+// internal helper to lazily create a buffer if necessary
+func bufApp(buf *[]byte, s string, w int, c byte) {
+	if *buf == nil {
+		if s[w] == c {
+			return
+		}
+
+		*buf = make([]byte, len(s))
+		copy(*buf, s[:w])
+	}
+	(*buf)[w] = c
+}

+ 92 - 0
path_test.go

@@ -0,0 +1,92 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Based on the path package, Copyright 2009 The Go Authors.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package gin
+
+import (
+	"runtime"
+	"testing"
+)
+
+var cleanTests = []struct {
+	path, result string
+}{
+	// Already clean
+	{"/", "/"},
+	{"/abc", "/abc"},
+	{"/a/b/c", "/a/b/c"},
+	{"/abc/", "/abc/"},
+	{"/a/b/c/", "/a/b/c/"},
+
+	// missing root
+	{"", "/"},
+	{"abc", "/abc"},
+	{"abc/def", "/abc/def"},
+	{"a/b/c", "/a/b/c"},
+
+	// Remove doubled slash
+	{"//", "/"},
+	{"/abc//", "/abc/"},
+	{"/abc/def//", "/abc/def/"},
+	{"/a/b/c//", "/a/b/c/"},
+	{"/abc//def//ghi", "/abc/def/ghi"},
+	{"//abc", "/abc"},
+	{"///abc", "/abc"},
+	{"//abc//", "/abc/"},
+
+	// Remove . elements
+	{".", "/"},
+	{"./", "/"},
+	{"/abc/./def", "/abc/def"},
+	{"/./abc/def", "/abc/def"},
+	{"/abc/.", "/abc/"},
+
+	// Remove .. elements
+	{"..", "/"},
+	{"../", "/"},
+	{"../../", "/"},
+	{"../..", "/"},
+	{"../../abc", "/abc"},
+	{"/abc/def/ghi/../jkl", "/abc/def/jkl"},
+	{"/abc/def/../ghi/../jkl", "/abc/jkl"},
+	{"/abc/def/..", "/abc"},
+	{"/abc/def/../..", "/"},
+	{"/abc/def/../../..", "/"},
+	{"/abc/def/../../..", "/"},
+	{"/abc/def/../../../ghi/jkl/../../../mno", "/mno"},
+
+	// Combinations
+	{"abc/./../def", "/def"},
+	{"abc//./../def", "/def"},
+	{"abc/../../././../def", "/def"},
+}
+
+func TestPathClean(t *testing.T) {
+	for _, test := range cleanTests {
+		if s := CleanPath(test.path); s != test.result {
+			t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result)
+		}
+		if s := CleanPath(test.result); s != test.result {
+			t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result)
+		}
+	}
+}
+
+func TestPathCleanMallocs(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping malloc count in short mode")
+	}
+	if runtime.GOMAXPROCS(0) > 1 {
+		t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
+		return
+	}
+
+	for _, test := range cleanTests {
+		allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) })
+		if allocs > 0 {
+			t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs)
+		}
+	}
+}

+ 1 - 9
routergroup.go

@@ -7,8 +7,6 @@ package gin
 import (
 	"net/http"
 	"path"
-
-	"github.com/julienschmidt/httprouter"
 )
 
 // Used internally to configure router, a RouterGroup is associated with a prefix
@@ -48,13 +46,7 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []Han
 	absolutePath := group.calculateAbsolutePath(relativePath)
 	handlers = group.combineHandlers(handlers)
 	debugRoute(httpMethod, absolutePath, handlers)
-
-	group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
-		context := group.engine.createContext(w, req, params, handlers)
-		context.Next()
-		context.Writer.WriteHeaderNow()
-		group.engine.reuseContext(context)
-	})
+	group.engine.handle(httpMethod, absolutePath, handlers)
 }
 
 // POST is a shortcut for router.Handle("POST", path, handle)

+ 556 - 0
tree.go

@@ -0,0 +1,556 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package gin
+
+import (
+	"strings"
+	"unicode"
+)
+
+func min(a, b int) int {
+	if a <= b {
+		return a
+	}
+	return b
+}
+
+func countParams(path string) uint8 {
+	var n uint
+	for i := 0; i < len(path); i++ {
+		if path[i] != ':' && path[i] != '*' {
+			continue
+		}
+		n++
+	}
+	if n >= 255 {
+		return 255
+	}
+	return uint8(n)
+}
+
+type nodeType uint8
+
+const (
+	static   nodeType = 0
+	param    nodeType = 1
+	catchAll nodeType = 2
+)
+
+type node struct {
+	path      string
+	wildChild bool
+	nType     nodeType
+	maxParams uint8
+	indices   string
+	children  []*node
+	handlers  []HandlerFunc
+	priority  uint32
+}
+
+// increments priority of the given child and reorders if necessary
+func (n *node) incrementChildPrio(pos int) int {
+	n.children[pos].priority++
+	prio := n.children[pos].priority
+
+	// adjust position (move to front)
+	newPos := pos
+	for newPos > 0 && n.children[newPos-1].priority < prio {
+		// swap node positions
+		tmpN := n.children[newPos-1]
+		n.children[newPos-1] = n.children[newPos]
+		n.children[newPos] = tmpN
+
+		newPos--
+	}
+
+	// build new index char string
+	if newPos != pos {
+		n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
+			n.indices[pos:pos+1] + // the index char we move
+			n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
+	}
+
+	return newPos
+}
+
+// addRoute adds a node with the given handle to the path.
+// Not concurrency-safe!
+func (n *node) addRoute(path string, handlers []HandlerFunc) {
+	n.priority++
+	numParams := countParams(path)
+
+	// non-empty tree
+	if len(n.path) > 0 || len(n.children) > 0 {
+	walk:
+		for {
+			// Update maxParams of the current node
+			if numParams > n.maxParams {
+				n.maxParams = numParams
+			}
+
+			// Find the longest common prefix.
+			// This also implies that the common prefix contains no ':' or '*'
+			// since the existing key can't contain those chars.
+			i := 0
+			max := min(len(path), len(n.path))
+			for i < max && path[i] == n.path[i] {
+				i++
+			}
+
+			// Split edge
+			if i < len(n.path) {
+				child := node{
+					path:      n.path[i:],
+					wildChild: n.wildChild,
+					indices:   n.indices,
+					children:  n.children,
+					handlers:  n.handlers,
+					priority:  n.priority - 1,
+				}
+
+				// Update maxParams (max of all children)
+				for i := range child.children {
+					if child.children[i].maxParams > child.maxParams {
+						child.maxParams = child.children[i].maxParams
+					}
+				}
+
+				n.children = []*node{&child}
+				// []byte for proper unicode char conversion, see #65
+				n.indices = string([]byte{n.path[i]})
+				n.path = path[:i]
+				n.handlers = nil
+				n.wildChild = false
+			}
+
+			// Make new node a child of this node
+			if i < len(path) {
+				path = path[i:]
+
+				if n.wildChild {
+					n = n.children[0]
+					n.priority++
+
+					// Update maxParams of the child node
+					if numParams > n.maxParams {
+						n.maxParams = numParams
+					}
+					numParams--
+
+					// Check if the wildcard matches
+					if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
+						// check for longer wildcard, e.g. :name and :names
+						if len(n.path) >= len(path) || path[len(n.path)] == '/' {
+							continue walk
+						}
+					}
+
+					panic("conflict with wildcard route")
+				}
+
+				c := path[0]
+
+				// slash after param
+				if n.nType == param && c == '/' && len(n.children) == 1 {
+					n = n.children[0]
+					n.priority++
+					continue walk
+				}
+
+				// Check if a child with the next path byte exists
+				for i := 0; i < len(n.indices); i++ {
+					if c == n.indices[i] {
+						i = n.incrementChildPrio(i)
+						n = n.children[i]
+						continue walk
+					}
+				}
+
+				// Otherwise insert it
+				if c != ':' && c != '*' {
+					// []byte for proper unicode char conversion, see #65
+					n.indices += string([]byte{c})
+					child := &node{
+						maxParams: numParams,
+					}
+					n.children = append(n.children, child)
+					n.incrementChildPrio(len(n.indices) - 1)
+					n = child
+				}
+				n.insertChild(numParams, path, handlers)
+				return
+
+			} else if i == len(path) { // Make node a (in-path) leaf
+				if n.handlers != nil {
+					panic("a Handle is already registered for this path")
+				}
+				n.handlers = handlers
+			}
+			return
+		}
+	} else { // Empty tree
+		n.insertChild(numParams, path, handlers)
+	}
+}
+
+func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) {
+	var offset int // already handled bytes of the path
+
+	// find prefix until first wildcard (beginning with ':'' or '*'')
+	for i, max := 0, len(path); numParams > 0; i++ {
+		c := path[i]
+		if c != ':' && c != '*' {
+			continue
+		}
+
+		// check if this Node existing children which would be
+		// unreachable if we insert the wildcard here
+		if len(n.children) > 0 {
+			panic("wildcard route conflicts with existing children")
+		}
+
+		// find wildcard end (either '/' or path end)
+		end := i + 1
+		for end < max && path[end] != '/' {
+			switch path[end] {
+			// the wildcard name must not contain ':' and '*'
+			case ':', '*':
+				panic("only one wildcard per path segment is allowed")
+			default:
+				end++
+			}
+		}
+
+		// check if the wildcard has a name
+		if end-i < 2 {
+			panic("wildcards must be named with a non-empty name")
+		}
+
+		if c == ':' { // param
+			// split path at the beginning of the wildcard
+			if i > 0 {
+				n.path = path[offset:i]
+				offset = i
+			}
+
+			child := &node{
+				nType:     param,
+				maxParams: numParams,
+			}
+			n.children = []*node{child}
+			n.wildChild = true
+			n = child
+			n.priority++
+			numParams--
+
+			// if the path doesn't end with the wildcard, then there
+			// will be another non-wildcard subpath starting with '/'
+			if end < max {
+				n.path = path[offset:end]
+				offset = end
+
+				child := &node{
+					maxParams: numParams,
+					priority:  1,
+				}
+				n.children = []*node{child}
+				n = child
+			}
+
+		} else { // catchAll
+			if end != max || numParams > 1 {
+				panic("catch-all routes are only allowed at the end of the path")
+			}
+
+			if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
+				panic("catch-all conflicts with existing handle for the path segment root")
+			}
+
+			// currently fixed width 1 for '/'
+			i--
+			if path[i] != '/' {
+				panic("no / before catch-all")
+			}
+
+			n.path = path[offset:i]
+
+			// first node: catchAll node with empty path
+			child := &node{
+				wildChild: true,
+				nType:     catchAll,
+				maxParams: 1,
+			}
+			n.children = []*node{child}
+			n.indices = string(path[i])
+			n = child
+			n.priority++
+
+			// second node: node holding the variable
+			child = &node{
+				path:      path[i:],
+				nType:     catchAll,
+				maxParams: 1,
+				handlers:  handlers,
+				priority:  1,
+			}
+			n.children = []*node{child}
+
+			return
+		}
+	}
+
+	// insert remaining path part and handle to the leaf
+	n.path = path[offset:]
+	n.handlers = handlers
+}
+
+// Returns the handle registered with the given path (key). The values of
+// wildcards are saved to a map.
+// If no handle can be found, a TSR (trailing slash redirect) recommendation is
+// made if a handle exists with an extra (without the) trailing slash for the
+// given path.
+func (n *node) getValue(path string, po Params) (handlers []HandlerFunc, p Params, tsr bool) {
+walk: // Outer loop for walking the tree
+	for {
+		if len(path) > len(n.path) {
+			if path[:len(n.path)] == n.path {
+				path = path[len(n.path):]
+				// If this node does not have a wildcard (param or catchAll)
+				// child,  we can just look up the next child node and continue
+				// to walk down the tree
+				if !n.wildChild {
+					c := path[0]
+					for i := 0; i < len(n.indices); i++ {
+						if c == n.indices[i] {
+							n = n.children[i]
+							continue walk
+						}
+					}
+
+					// Nothing found.
+					// We can recommend to redirect to the same URL without a
+					// trailing slash if a leaf exists for that path.
+					tsr = (path == "/" && n.handlers != nil)
+					return
+
+				}
+
+				// handle wildcard child
+				n = n.children[0]
+				switch n.nType {
+				case param:
+					// find param end (either '/' or path end)
+					end := 0
+					for end < len(path) && path[end] != '/' {
+						end++
+					}
+
+					// save param value
+					if p == nil {
+						if cap(po) < int(n.maxParams) {
+							p = make(Params, 0, n.maxParams)
+						} else {
+							p = po[0:0]
+						}
+					}
+					i := len(p)
+					p = p[:i+1] // expand slice within preallocated capacity
+					p[i].Key = n.path[1:]
+					p[i].Value = path[:end]
+
+					// we need to go deeper!
+					if end < len(path) {
+						if len(n.children) > 0 {
+							path = path[end:]
+							n = n.children[0]
+							continue walk
+						}
+
+						// ... but we can't
+						tsr = (len(path) == end+1)
+						return
+					}
+
+					if handlers = n.handlers; handlers != nil {
+						return
+					} else if len(n.children) == 1 {
+						// No handle found. Check if a handle for this path + a
+						// trailing slash exists for TSR recommendation
+						n = n.children[0]
+						tsr = (n.path == "/" && n.handlers != nil)
+					}
+
+					return
+
+				case catchAll:
+					// save param value
+					if p == nil {
+						if cap(po) < int(n.maxParams) {
+							p = make(Params, 0, n.maxParams)
+						} else {
+							p = po[0:0]
+						}
+					}
+					i := len(p)
+					p = p[:i+1] // expand slice within preallocated capacity
+					p[i].Key = n.path[2:]
+					p[i].Value = path
+
+					handlers = n.handlers
+					return
+
+				default:
+					panic("Invalid node type")
+				}
+			}
+		} else if path == n.path {
+			// We should have reached the node containing the handle.
+			// Check if this node has a handle registered.
+			if handlers = n.handlers; handlers != nil {
+				return
+			}
+
+			// No handle found. Check if a handle for this path + a
+			// trailing slash exists for trailing slash recommendation
+			for i := 0; i < len(n.indices); i++ {
+				if n.indices[i] == '/' {
+					n = n.children[i]
+					tsr = (len(n.path) == 1 && n.handlers != nil) ||
+						(n.nType == catchAll && n.children[0].handlers != nil)
+					return
+				}
+			}
+
+			return
+		}
+
+		// Nothing found. We can recommend to redirect to the same URL with an
+		// extra trailing slash if a leaf exists for that path
+		tsr = (path == "/") ||
+			(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
+				path == n.path[:len(n.path)-1] && n.handlers != nil)
+		return
+	}
+}
+
+// Makes a case-insensitive lookup of the given path and tries to find a handler.
+// It can optionally also fix trailing slashes.
+// It returns the case-corrected path and a bool indicating whether the lookup
+// was successful.
+func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
+	ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
+
+	// Outer loop for walking the tree
+	for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) {
+		path = path[len(n.path):]
+		ciPath = append(ciPath, n.path...)
+
+		if len(path) > 0 {
+			// If this node does not have a wildcard (param or catchAll) child,
+			// we can just look up the next child node and continue to walk down
+			// the tree
+			if !n.wildChild {
+				r := unicode.ToLower(rune(path[0]))
+				for i, index := range n.indices {
+					// must use recursive approach since both index and
+					// ToLower(index) could exist. We must check both.
+					if r == unicode.ToLower(index) {
+						out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash)
+						if found {
+							return append(ciPath, out...), true
+						}
+					}
+				}
+
+				// Nothing found. We can recommend to redirect to the same URL
+				// without a trailing slash if a leaf exists for that path
+				found = (fixTrailingSlash && path == "/" && n.handlers != nil)
+				return
+			}
+
+			n = n.children[0]
+			switch n.nType {
+			case param:
+				// find param end (either '/' or path end)
+				k := 0
+				for k < len(path) && path[k] != '/' {
+					k++
+				}
+
+				// add param value to case insensitive path
+				ciPath = append(ciPath, path[:k]...)
+
+				// we need to go deeper!
+				if k < len(path) {
+					if len(n.children) > 0 {
+						path = path[k:]
+						n = n.children[0]
+						continue
+					}
+
+					// ... but we can't
+					if fixTrailingSlash && len(path) == k+1 {
+						return ciPath, true
+					}
+					return
+				}
+
+				if n.handlers != nil {
+					return ciPath, true
+				} else if fixTrailingSlash && len(n.children) == 1 {
+					// No handle found. Check if a handle for this path + a
+					// trailing slash exists
+					n = n.children[0]
+					if n.path == "/" && n.handlers != nil {
+						return append(ciPath, '/'), true
+					}
+				}
+				return
+
+			case catchAll:
+				return append(ciPath, path...), true
+
+			default:
+				panic("Invalid node type")
+			}
+		} else {
+			// We should have reached the node containing the handle.
+			// Check if this node has a handle registered.
+			if n.handlers != nil {
+				return ciPath, true
+			}
+
+			// No handle found.
+			// Try to fix the path by adding a trailing slash
+			if fixTrailingSlash {
+				for i := 0; i < len(n.indices); i++ {
+					if n.indices[i] == '/' {
+						n = n.children[i]
+						if (len(n.path) == 1 && n.handlers != nil) ||
+							(n.nType == catchAll && n.children[0].handlers != nil) {
+							return append(ciPath, '/'), true
+						}
+						return
+					}
+				}
+			}
+			return
+		}
+	}
+
+	// Nothing found.
+	// Try to fix the path by adding / removing a trailing slash
+	if fixTrailingSlash {
+		if path == "/" {
+			return ciPath, true
+		}
+		if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
+			strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) &&
+			n.handlers != nil {
+			return append(ciPath, n.path...), true
+		}
+	}
+	return
+}

+ 35 - 0
utils.go

@@ -12,6 +12,18 @@ import (
 	"strings"
 )
 
+const (
+	methodGET     = iota
+	methodPOST    = iota
+	methodPUT     = iota
+	methodAHEAD   = iota
+	methodOPTIONS = iota
+	methodDELETE  = iota
+	methodCONNECT = iota
+	methodTRACE   = iota
+	methodUnknown = iota
+)
+
 type H map[string]interface{}
 
 // Allows type H to be used with xml.Marshal
@@ -80,3 +92,26 @@ func lastChar(str string) uint8 {
 func nameOfFunction(f interface{}) string {
 	return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
 }
+
+func codeForHTTPMethod(method string) int {
+	switch method {
+	case "GET":
+		return methodGET
+	case "POST":
+		return methodPOST
+	case "PUT":
+		return methodPUT
+	case "AHEAD":
+		return methodAHEAD
+	case "OPTIONS":
+		return methodOPTIONS
+	case "DELETE":
+		return methodDELETE
+	case "TRACE":
+		return methodTRACE
+	case "CONNECT":
+		return methodCONNECT
+	default:
+		return methodUnknown
+	}
+}