From 8ee40b409a9bd857ac592f59b178fcf8a5412d25 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 00:10:37 +0800 Subject: [PATCH 01/22] update --- router.go | 195 ++++++---------- tree.go | 681 +++++++++++++++++++++++++++--------------------------- 2 files changed, 413 insertions(+), 463 deletions(-) diff --git a/router.go b/router.go index 1eab403d..1f8c4ebf 100644 --- a/router.go +++ b/router.go @@ -34,7 +34,7 @@ // 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 // request to that function. -// For the methods GET, POST, PUT, PATCH, DELETE and OPTIONS shortcut functions exist to +// For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to // register handles, for all other methods router.Handle can be used. // // The registered path, against which the router matches incoming requests, can @@ -77,17 +77,24 @@ package httprouter import ( + "io" + "os" + + "fmt" "context" "net/http" "strings" - "sync" -) + "reflect" + "runtime" + ) // Handle is a function that can be registered to a route to handle HTTP // requests. Like http.HandlerFunc, but has a third parameter for the values of -// wildcards (path variables). +// wildcards (variables). type Handle func(http.ResponseWriter, *http.Request, Params) +var DefaultWriter io.Writer = os.Stdout + // Param is a single URL parameter, consisting of a key and a value. type Param struct { Key string @@ -102,9 +109,9 @@ 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 _, p := range ps { - if p.Key == name { - return p.Value + for i := range ps { + if ps[i].Key == name { + return ps[i].Value } } return "" @@ -122,36 +129,16 @@ func ParamsFromContext(ctx context.Context) Params { return p } -// MatchedRoutePathParam is the Param name under which the path of the matched -// route is stored, if Router.SaveMatchedRoutePath is set. -var MatchedRoutePathParam = "$matchedRoutePath" - -// MatchedRoutePath retrieves the path of the matched route. -// Router.SaveMatchedRoutePath must have been enabled when the respective -// handler was added, otherwise this function always returns an empty string. -func (ps Params) MatchedRoutePath() string { - return ps.ByName(MatchedRoutePathParam) -} - // Router is a http.Handler which can be used to dispatch requests to different // handler functions via configurable routes type Router struct { trees map[string]*node - paramsPool sync.Pool - maxParams uint16 - - // If enabled, adds the matched route path onto the http.Request context - // before invoking the handler. - // The matched route path is only added to handlers of routes that were - // registered when this option was enabled. - SaveMatchedRoutePath bool - // 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 308 for all other request methods. + // and 307 for all other request methods. RedirectTrailingSlash bool // If enabled, the router tries to fix the current request path, if no @@ -159,7 +146,7 @@ type Router struct { // 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 308 for + // 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. @@ -219,68 +206,47 @@ func New() *Router { } } -func (r *Router) getParams() *Params { - ps, _ := r.paramsPool.Get().(*Params) - *ps = (*ps)[0:0] // reset slice - return ps -} - -func (r *Router) putParams(ps *Params) { - if ps != nil { - r.paramsPool.Put(ps) - } -} - -func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle { - return func(w http.ResponseWriter, req *http.Request, ps Params) { - if ps == nil { - psp := r.getParams() - ps = (*psp)[0:1] - ps[0] = Param{Key: MatchedRoutePathParam, Value: path} - handle(w, req, ps) - r.putParams(psp) - } else { - ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path}) - handle(w, req, ps) - } - } -} - // GET is a shortcut for router.Handle(http.MethodGet, path, handle) -func (r *Router) GET(path string, handle Handle) { - r.Handle(http.MethodGet, path, handle) +func (r *Router) GET(path string, handle ...Handle) { + r.Handle(http.MethodGet, path, handle...) } // HEAD is a shortcut for router.Handle(http.MethodHead, path, handle) -func (r *Router) HEAD(path string, handle Handle) { - r.Handle(http.MethodHead, path, handle) +func (r *Router) HEAD(path string, handle ...Handle) { + r.Handle(http.MethodHead, path, handle...) } // OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle) -func (r *Router) OPTIONS(path string, handle Handle) { - r.Handle(http.MethodOptions, path, handle) +func (r *Router) OPTIONS(path string, handle ...Handle) { + r.Handle(http.MethodOptions, path, handle...) } // POST is a shortcut for router.Handle(http.MethodPost, path, handle) -func (r *Router) POST(path string, handle Handle) { - r.Handle(http.MethodPost, path, handle) +func (r *Router) POST(path string, handle ...Handle) { + r.Handle(http.MethodPost, path, handle...) } // PUT is a shortcut for router.Handle(http.MethodPut, path, handle) -func (r *Router) PUT(path string, handle Handle) { - r.Handle(http.MethodPut, path, handle) +func (r *Router) PUT(path string, handle ...Handle) { + r.Handle(http.MethodPut, path, handle...) } // PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle) -func (r *Router) PATCH(path string, handle Handle) { - r.Handle(http.MethodPatch, path, handle) +func (r *Router) PATCH(path string, handle ...Handle) { + r.Handle(http.MethodPatch, path, handle...) } // DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle) -func (r *Router) DELETE(path string, handle Handle) { - r.Handle(http.MethodDelete, path, handle) +func (r *Router) DELETE(path string, handle ...Handle) { + r.Handle(http.MethodDelete, path, handle...) } +func If(condition bool, trueVal, falseVal interface{}) interface{} { + if condition { + return trueVal + } + return falseVal +} // Handle registers a new request handle with the given path and method. // // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut @@ -289,28 +255,29 @@ func (r *Router) DELETE(path string, handle Handle) { // This function is intended for bulk loading and to allow the usage of less // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). -func (r *Router) Handle(method, path string, handle Handle) { - varsCount := uint16(0) - - if method == "" { - panic("method must not be empty") - } +func (r *Router) Handle(method, path string, handles ...Handle) { + //for _, handle := range handles if len(path) < 1 || path[0] != '/' { panic("path must begin with '/' in path '" + path + "'") } - if handle == nil { - panic("handle must not be nil") - } - - if r.SaveMatchedRoutePath { - varsCount++ - handle = r.saveMatchedRoutePath(path, handle) - } if r.trees == nil { r.trees = make(map[string]*node) } + //fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> %s (%d handlers)\n", method, path, handlerName, nuHandlers) + handleNames := "" + for _, handle := range handles { + //handleNames = If(len(handleNames) > 0 ? handleNames+",","")+runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() + if len(handleNames) > 0 { + handleNames = handleNames + "," + runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() + }else{ + handleNames = runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() + } + //runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() + } + fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> [%s] (%d handlers)\n", method, path,handleNames,len(handles) ) + root := r.trees[method] if root == nil { root = new(node) @@ -318,21 +285,7 @@ func (r *Router) Handle(method, path string, handle Handle) { r.globalAllowed = r.allowed("*", "") } - - root.addRoute(path, handle) - - // Update maxParams - if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams { - r.maxParams = paramsCount + varsCount - } - - // Lazy-init paramsPool alloc func - if r.paramsPool.New == nil && r.maxParams > 0 { - r.paramsPool.New = func() interface{} { - ps := make(Params, 0, r.maxParams) - return &ps - } - } + root.addRoute(path, handles) } // Handler is an adapter which allows the usage of an http.Handler as a @@ -391,17 +344,9 @@ func (r *Router) recv(w http.ResponseWriter, req *http.Request) { // If the path was found, it returns the handle function and the path parameter // values. Otherwise the third return value indicates whether a redirection to // the same path with an extra / without the trailing slash should be performed. -func (r *Router) Lookup(method, path string) (Handle, Params, bool) { +func (r *Router) Lookup(method, path string) (HandlersChain, Params, bool) { if root := r.trees[method]; root != nil { - handle, ps, tsr := root.getValue(path, r.getParams) - if handle == nil { - r.putParams(ps) - return nil, nil, tsr - } - if ps == nil { - return handle, nil, tsr - } - return handle, *ps, tsr + return root.getValue(path) } return nil, nil, false } @@ -429,7 +374,7 @@ func (r *Router) allowed(path, reqMethod string) (allow string) { continue } - handle, _, _ := r.trees[method].getValue(path, nil) + handle, _, _ := r.trees[method].getValue(path) if handle != nil { // Add request method to list of allowed methods allowed = append(allowed, method) @@ -453,8 +398,7 @@ func (r *Router) allowed(path, reqMethod string) (allow string) { // return as comma separated list return strings.Join(allowed, ", ") } - - return allow + return } // ServeHTTP makes the router implement the http.Handler interface. @@ -464,22 +408,25 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { } path := req.URL.Path + //输出请求的处理日志 + reqLog := fmt.Sprintf("[http-router] %-6s req.uri=%-12s", req.Method,req.RequestURI) if root := r.trees[req.Method]; root != nil { - if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil { - if ps != nil { - handle(w, req, *ps) - r.putParams(ps) - } else { - handle(w, req, nil) + if handles, ps, tsr := root.getValue(path); handles != nil { + for _,handle := range handles { + //输出请求的处理日志 + fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) + handle(w, req, ps) } return } else if req.Method != http.MethodConnect && path != "/" { - // Moved Permanently, request with GET method - code := http.StatusMovedPermanently + fmt.Fprintf(DefaultWriter, reqLog) + + code := 301 // Permanent redirect, request with GET method if req.Method != http.MethodGet { - // Permanent Redirect, request with same method - code = http.StatusPermanentRedirect + // Temporary redirect, request with same method + // As of Go 1.3, Go does not support status code 308. + code = 307 } if tsr && r.RedirectTrailingSlash { @@ -499,12 +446,14 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.RedirectTrailingSlash, ) if found { - req.URL.Path = fixedPath + req.URL.Path = string(fixedPath) http.Redirect(w, req, req.URL.String(), code) return } } } + }else{ + fmt.Fprintf(DefaultWriter, reqLog) } if req.Method == http.MethodOptions && r.HandleOPTIONS { diff --git a/tree.go b/tree.go index 6eb4fe67..0b741101 100644 --- a/tree.go +++ b/tree.go @@ -17,49 +17,21 @@ func min(a, b int) int { return b } -func longestCommonPrefix(a, b string) int { - i := 0 - max := min(len(a), len(b)) - for i < max && a[i] == b[i] { - i++ - } - return i -} +const maxParamCount uint8 = ^uint8(0) -// Search for a wildcard segment and check the name for invalid characters. -// Returns -1 as index, if no wildcard was found. -func findWildcard(path string) (wilcard string, i int, valid bool) { - // Find start - for start, c := range []byte(path) { - // A wildcard starts with ':' (param) or '*' (catch-all) - if c != ':' && c != '*' { +func countParams(path string) uint8 { + var n uint + for i := 0; i < len(path); i++ { + if path[i] != ':' && path[i] != '*' { continue } - - // Find end and check for invalid characters - valid = true - for end, c := range []byte(path[start+1:]) { - switch c { - case '/': - return path[start : start+1+end], start, valid - case ':', '*': - valid = false - } - } - return path[start:], start, valid + n++ } - return "", -1, false -} - -func countParams(path string) uint16 { - var n uint - for i := range []byte(path) { - switch path[i] { - case ':', '*': - n++ - } + if n >= uint(maxParamCount) { + return maxParamCount } - return uint16(n) + + return uint8(n) } type nodeType uint8 @@ -71,34 +43,51 @@ const ( catchAll ) +//增加调用链特性:HandlersChain defines a HandlerFunc array. +type HandlersChain []Handle +/** +http://c.biancheng.net/view/5403.html +radix 的节点类型为 *httprouter.node ,为了说明方便,我们留下了目前关心的几个字段: +path:当前节点对应的路径中的字符串 +wildChild:子节点是否为参数节点,即 wildcard node,或者说 :id 这种类型的节点 +nType:当前节点类型,有四个枚举值: 分别为 static/root/param/catchAll。 +static // 非根节点的普通字符串节点 +root // 根节点 +param // 参数节点,例如 :id +catchAll // 通配符节点,例如 *anyway + +indices:子节点索引,当子节点为非参数类型,即本节点的 wildChild 为 false 时,会将每个子节点的首字母放在该索引数组。说是数组,实际上是个 string。 + */ type node struct { path string - indices string wildChild bool nType nodeType + maxParams uint8 priority uint32 + indices string children []*node - handle Handle + handles HandlersChain } -// Increments priority of the given child and reorders if necessary +// increments priority of the given child and reorders if necessary func (n *node) incrementChildPrio(pos int) int { - cs := n.children - cs[pos].priority++ - prio := cs[pos].priority + n.children[pos].priority++ + prio := n.children[pos].priority - // Adjust position (move to front) + // adjust position (move to front) newPos := pos - for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { - // Swap node positions - cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] + for newPos > 0 && n.children[newPos-1].priority < prio { + // swap node positions + n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] + + newPos-- } - // Build new index char string + // 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' + 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 @@ -106,216 +95,255 @@ func (n *node) incrementChildPrio(pos int) int { // addRoute adds a node with the given handle to the path. // Not concurrency-safe! -func (n *node) addRoute(path string, handle Handle) { +func (n *node) addRoute(path string, handles HandlersChain) { fullPath := path n.priority++ - - // Empty tree - if n.path == "" && n.indices == "" { - n.insertChild(path, fullPath, handle) - n.nType = root - return - } - -walk: - for { - // 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 := longestCommonPrefix(path, n.path) - - // Split edge - if i < len(n.path) { - child := node{ - path: n.path[i:], - wildChild: n.wildChild, - nType: static, - indices: n.indices, - children: n.children, - handle: n.handle, - priority: n.priority - 1, + 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 } - n.children = []*node{&child} - // []byte for proper unicode char conversion, see #65 - n.indices = string([]byte{n.path[i]}) - n.path = path[:i] - n.handle = nil - n.wildChild = false - } + // 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++ + } - // Make new node a child of this node - if i < len(path) { - path = path[i:] + // Split edge + if i < len(n.path) { + child := node{ + path: n.path[i:], + wildChild: n.wildChild, + nType: static, + indices: n.indices, + children: n.children, + handles: n.handles, + priority: n.priority - 1, + } - if n.wildChild { - n = n.children[0] - n.priority++ - - // Check if the wildcard matches - if len(path) >= len(n.path) && n.path == path[:len(n.path)] && - // Adding a child to a catchAll is not possible - n.nType != catchAll && - // Check for longer wildcard, e.g. :name and :names - (len(n.path) >= len(path) || path[len(n.path)] == '/') { - continue walk - } else { - // Wildcard conflict - pathSeg := path - if n.nType != catchAll { - pathSeg = strings.SplitN(pathSeg, "/", 2)[0] + // Update maxParams (max of all children) + for i := range child.children { + if child.children[i].maxParams > child.maxParams { + child.maxParams = child.children[i].maxParams } - prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path - panic("'" + pathSeg + - "' in new path '" + fullPath + - "' conflicts with existing wildcard '" + n.path + - "' in existing prefix '" + prefix + - "'") } + + n.children = []*node{&child} + // []byte for proper unicode char conversion, see #65 + n.indices = string([]byte{n.path[i]}) + n.path = path[:i] + n.handles = nil + n.wildChild = false } - idxc := path[0] + // Make new node a child of this node + if i < len(path) { + path = path[i:] - // '/' after param - if n.nType == param && idxc == '/' && len(n.children) == 1 { - n = n.children[0] - n.priority++ - continue walk - } + if n.wildChild { + n = n.children[0] + n.priority++ - // Check if a child with the next path byte exists - for i, c := range []byte(n.indices) { - if c == idxc { - i = n.incrementChildPrio(i) - n = n.children[i] + // 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)] && + // Adding a child to a catchAll is not possible + n.nType != catchAll && + // Check for longer wildcard, e.g. :name and :names + (len(n.path) >= len(path) || path[len(n.path)] == '/') { + continue walk + } else { + // Wildcard conflict + var pathSeg string + if n.nType == catchAll { + pathSeg = path + } else { + pathSeg = strings.SplitN(path, "/", 2)[0] + } + prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path + panic("'" + pathSeg + + "' in new path '" + fullPath + + "' conflicts with existing wildcard '" + n.path + + "' in existing prefix '" + prefix + + "'") + } + } + + c := path[0] + + // slash after param + if n.nType == param && c == '/' && len(n.children) == 1 { + n = n.children[0] + n.priority++ continue walk } - } - // Otherwise insert it - if idxc != ':' && idxc != '*' { - // []byte for proper unicode char conversion, see #65 - n.indices += string([]byte{idxc}) - child := &node{} - n.children = append(n.children, child) - n.incrementChildPrio(len(n.indices) - 1) - n = child + // 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, fullPath, handles) + return + + } else if i == len(path) { // Make node a (in-path) leaf + if n.handles != nil { + panic("a handle is already registered for path '" + fullPath + "'") + } + n.handles = handles } - n.insertChild(path, fullPath, handle) return } - - // Otherwise add handle to current node - if n.handle != nil { - panic("a handle is already registered for path '" + fullPath + "'") - } - n.handle = handle - return + } else { // Empty tree + n.insertChild(numParams, path, fullPath, handles) + n.nType = root } } -func (n *node) insertChild(path, fullPath string, handle Handle) { - for { - // Find prefix until first wildcard - wildcard, i, valid := findWildcard(path) - if i < 0 { // No wilcard found - break - } +func (n *node) insertChild(numParams uint8, path, fullPath string, handles HandlersChain) { + var offset int // already handled bytes of the path - // The wildcard name must not contain ':' and '*' - if !valid { - panic("only one wildcard per path segment is allowed, has: '" + - wildcard + "' in path '" + fullPath + "'") + // 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 the wildcard has a name - if len(wildcard) < 2 { - panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") + // 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, has: '" + + path[i:] + "' in path '" + fullPath + "'") + default: + end++ + } } - // Check if this node has existing children which would be + // check if this Node existing children which would be // unreachable if we insert the wildcard here if len(n.children) > 0 { - panic("wildcard segment '" + wildcard + + panic("wildcard route '" + path[i:end] + "' conflicts with existing children in path '" + fullPath + "'") } - // param - if wildcard[0] == ':' { + // check if the wildcard has a name + if end-i < 2 { + panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") + } + + if c == ':' { // param + // split path at the beginning of the wildcard if i > 0 { - // Insert prefix before the current wildcard - n.path = path[:i] - path = path[i:] + n.path = path[offset:i] + offset = i } - n.wildChild = true child := &node{ - nType: param, - path: wildcard, + 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 + // if the path doesn't end with the wildcard, then there // will be another non-wildcard subpath starting with '/' - if len(wildcard) < len(path) { - path = path[len(wildcard):] + if end < max { + n.path = path[offset:end] + offset = end + child := &node{ - priority: 1, + maxParams: numParams, + priority: 1, } n.children = []*node{child} n = child - continue } - // Otherwise we're done. Insert the handle in the new leaf - n.handle = handle - return - } + } else { // catchAll + if end != max || numParams > 1 { + panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") + } - // catchAll - if i+len(wildcard) != len(path) { - panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") - } + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") + } - if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") - } + // currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all in path '" + fullPath + "'") + } - // Currently fixed width 1 for '/' - i-- - if path[i] != '/' { - panic("no / before catch-all in path '" + fullPath + "'") - } + n.path = path[offset:i] - n.path = path[:i] + // first node: catchAll node with empty path + child := &node{ + wildChild: true, + nType: catchAll, + maxParams: 1, + } + // update maxParams of the parent node + if n.maxParams < 1 { + n.maxParams = 1 + } + n.children = []*node{child} + n.indices = string(path[i]) + n = child + n.priority++ - // First node: catchAll node with empty path - child := &node{ - wildChild: true, - nType: catchAll, - } - n.children = []*node{child} - n.indices = string('/') - n = child - n.priority++ - - // Second node: node holding the variable - child = &node{ - path: path[i:], - nType: catchAll, - handle: handle, - priority: 1, - } - n.children = []*node{child} + // second node: node holding the variable + child = &node{ + path: path[i:], + nType: catchAll, + maxParams: 1, + handles: handles, + priority: 1, + } + n.children = []*node{child} - return + return + } } - // If no wildcard was found, simply insert the path and handle - n.path = path - n.handle = handle + // insert remaining path part and handle to the leaf + n.path = path[offset:] + n.handles = handles } // Returns the handle registered with the given path (key). The values of @@ -323,21 +351,19 @@ func (n *node) insertChild(path, fullPath string, handle Handle) { // 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, params func() *Params) (handle Handle, ps *Params, tsr bool) { -walk: // Outer loop for walking the tree +func (n *node) getValue(path string) (handles HandlersChain, p Params, tsr bool) { +walk: // outer loop for walking the tree for { - prefix := n.path - if len(path) > len(prefix) { - if path[:len(prefix)] == prefix { - path = path[len(prefix):] - + 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 + // child, we can just look up the next child node and continue // to walk down the tree if !n.wildChild { - idxc := path[0] - for i, c := range []byte(n.indices) { - if c == idxc { + c := path[0] + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { n = n.children[i] continue walk } @@ -346,35 +372,32 @@ walk: // Outer loop for walking the tree // 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.handle != nil) + tsr = (path == "/" && n.handles != nil) return + } - // Handle wildcard child + // handle wildcard child n = n.children[0] switch n.nType { case param: - // Find param end (either '/' or path end) + // find param end (either '/' or path end) end := 0 for end < len(path) && path[end] != '/' { end++ } - // Save param value - if params != nil { - if ps == nil { - ps = params() - } - // Expand slice within preallocated capacity - i := len(*ps) - *ps = (*ps)[:i+1] - (*ps)[i] = Param{ - Key: n.path[1:], - Value: path[:end], - } + // save param value + if p == nil { + // lazy allocation + p = make(Params, 0, n.maxParams) } + 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! + // we need to go deeper! if end < len(path) { if len(n.children) > 0 { path = path[end:] @@ -387,49 +410,42 @@ walk: // Outer loop for walking the tree return } - if handle = n.handle; handle != nil { + if handles = n.handles; handles != 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.handle != nil) || (n.path == "" && n.indices == "/") + tsr = (n.path == "/" && n.handles != nil) } return case catchAll: - // Save param value - if params != nil { - if ps == nil { - ps = params() - } - // Expand slice within preallocated capacity - i := len(*ps) - *ps = (*ps)[:i+1] - (*ps)[i] = Param{ - Key: n.path[2:], - Value: path, - } + // save param value + if p == nil { + // lazy allocation + p = make(Params, 0, n.maxParams) } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[2:] + p[i].Value = path - handle = n.handle + handles = n.handles return default: panic("invalid node type") } } - } else if path == prefix { + } else if path == n.path { // We should have reached the node containing the handle. // Check if this node has a handle registered. - if handle = n.handle; handle != nil { + if handles = n.handles; handles != nil { return } - // If there is no handle for this route, but this route has a - // wildcard child, there must be a handle for this path with an - // additional trailing slash if path == "/" && n.wildChild && n.nType != root { tsr = true return @@ -437,22 +453,23 @@ walk: // Outer loop for walking the tree // No handle found. Check if a handle for this path + a // trailing slash exists for trailing slash recommendation - for i, c := range []byte(n.indices) { - if c == '/' { + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { n = n.children[i] - tsr = (len(n.path) == 1 && n.handle != nil) || - (n.nType == catchAll && n.children[0].handle != nil) + tsr = (len(n.path) == 1 && n.handles != nil) || + (n.nType == catchAll && n.children[0].handles != 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(prefix) == len(path)+1 && prefix[len(path)] == '/' && - path == prefix[:len(prefix)-1] && n.handle != nil) + (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && + path == n.path[:len(n.path)-1] && n.handles != nil) return } } @@ -461,27 +478,16 @@ walk: // Outer loop for walking the tree // 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) (fixedPath string, found bool) { - const stackBufSize = 128 - - // Use a static sized buffer on the stack in the common case. - // If the path is too long, allocate a buffer on the heap instead. - buf := make([]byte, 0, stackBufSize) - if l := len(path) + 1; l > stackBufSize { - buf = make([]byte, 0, l) - } - - ciPath := n.findCaseInsensitivePathRec( +func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { + return n.findCaseInsensitivePathRec( path, - buf, // Preallocate enough memory for new path - [4]byte{}, // Empty rune buffer + make([]byte, 0, len(path)+1), // preallocate enough memory for new path + [4]byte{}, // empty rune buffer fixTrailingSlash, ) - - return string(ciPath), ciPath != nil } -// Shift bytes in array by n bytes left +// shift bytes in array by n bytes left func shiftNRuneBytes(rb [4]byte, n int) [4]byte { switch n { case 0: @@ -497,13 +503,14 @@ func shiftNRuneBytes(rb [4]byte, n int) [4]byte { } } -// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath -func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte { +// recursive case-insensitive lookup function used by n.findCaseInsensitivePath +func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) { npLen := len(n.path) -walk: // Outer loop for walking the tree +walk: // outer loop for walking the tree for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) { - // Add common prefix to result + // add common prefix to result + oldPath := path path = path[npLen:] ciPath = append(ciPath, n.path...) @@ -513,14 +520,13 @@ walk: // Outer loop for walking the tree // we can just look up the next child node and continue to walk down // the tree if !n.wildChild { - // Skip rune bytes already processed + // skip rune bytes already processed rb = shiftNRuneBytes(rb, npLen) if rb[0] != 0 { - // Old rune not finished - idxc := rb[0] - for i, c := range []byte(n.indices) { - if c == idxc { + // old rune not finished + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == rb[0] { // continue with child node n = n.children[i] npLen = len(n.path) @@ -528,12 +534,12 @@ walk: // Outer loop for walking the tree } } } else { - // Process a new rune + // process a new rune var rv rune - // Find rune start. - // Runes are up to 4 byte long, - // -4 would definitely be another rune. + // find rune start + // runes are up to 4 byte long, + // -4 would definitely be another rune var off int for max := min(npLen, 3); off < max; off++ { if i := npLen - off; utf8.RuneStart(oldPath[i]) { @@ -543,40 +549,38 @@ walk: // Outer loop for walking the tree } } - // Calculate lowercase bytes of current rune + // calculate lowercase bytes of current rune lo := unicode.ToLower(rv) utf8.EncodeRune(rb[:], lo) - // Skip already processed bytes + // skip already processed bytes rb = shiftNRuneBytes(rb, off) - idxc := rb[0] - for i, c := range []byte(n.indices) { - // Lowercase matches - if c == idxc { + for i := 0; i < len(n.indices); i++ { + // lowercase matches + if n.indices[i] == rb[0] { // must use a recursive approach since both the // uppercase byte and the lowercase byte might exist // as an index - if out := n.children[i].findCaseInsensitivePathRec( + if out, found := n.children[i].findCaseInsensitivePathRec( path, ciPath, rb, fixTrailingSlash, - ); out != nil { - return out + ); found { + return out, true } break } } - // If we found no match, the same for the uppercase rune, + // if we found no match, the same for the uppercase rune, // if it differs if up := unicode.ToUpper(rv); up != lo { utf8.EncodeRune(rb[:], up) rb = shiftNRuneBytes(rb, off) - idxc := rb[0] - for i, c := range []byte(n.indices) { - // Uppercase matches - if c == idxc { - // Continue with child node + for i, c := 0, rb[0]; i < len(n.indices); i++ { + // uppercase matches + if n.indices[i] == c { + // continue with child node n = n.children[i] npLen = len(n.path) continue walk @@ -587,55 +591,52 @@ walk: // Outer loop for walking the tree // Nothing found. We can recommend to redirect to the same URL // without a trailing slash if a leaf exists for that path - if fixTrailingSlash && path == "/" && n.handle != nil { - return ciPath - } - return nil + return ciPath, (fixTrailingSlash && path == "/" && n.handles != nil) } 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++ + // 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[:end]...) + // add param value to case insensitive path + ciPath = append(ciPath, path[:k]...) - // We need to go deeper! - if end < len(path) { + // we need to go deeper! + if k < len(path) { if len(n.children) > 0 { - // Continue with child node + // continue with child node n = n.children[0] npLen = len(n.path) - path = path[end:] + path = path[k:] continue } // ... but we can't - if fixTrailingSlash && len(path) == end+1 { - return ciPath + if fixTrailingSlash && len(path) == k+1 { + return ciPath, true } - return nil + return ciPath, false } - if n.handle != nil { - return ciPath + if n.handles != 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.handle != nil { - return append(ciPath, '/') + if n.path == "/" && n.handles != nil { + return append(ciPath, '/'), true } } - return nil + return ciPath, false case catchAll: - return append(ciPath, path...) + return append(ciPath, path...), true default: panic("invalid node type") @@ -643,25 +644,25 @@ walk: // Outer loop for walking the tree } else { // We should have reached the node containing the handle. // Check if this node has a handle registered. - if n.handle != nil { - return ciPath + if n.handles != nil { + return ciPath, true } // No handle found. // Try to fix the path by adding a trailing slash if fixTrailingSlash { - for i, c := range []byte(n.indices) { - if c == '/' { + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { n = n.children[i] - if (len(n.path) == 1 && n.handle != nil) || - (n.nType == catchAll && n.children[0].handle != nil) { - return append(ciPath, '/') + if (len(n.path) == 1 && n.handles != nil) || + (n.nType == catchAll && n.children[0].handles != nil) { + return append(ciPath, '/'), true } - return nil + return ciPath, false } } } - return nil + return ciPath, false } } @@ -669,12 +670,12 @@ walk: // Outer loop for walking the tree // Try to fix the path by adding / removing a trailing slash if fixTrailingSlash { if path == "/" { - return ciPath + return ciPath, true } if len(path)+1 == npLen && n.path[len(path)] == '/' && - strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil { - return append(ciPath, n.path...) + strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handles != nil { + return append(ciPath, n.path...), true } } - return nil + return ciPath, false } From 5e8877bd79aac47f5a9a41a70481077f3eee82e4 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 02:32:47 +0800 Subject: [PATCH 02/22] update --- router.go | 168 ++++++++++---- tree.go | 680 +++++++++++++++++++++++++++--------------------------- 2 files changed, 468 insertions(+), 380 deletions(-) diff --git a/router.go b/router.go index 1f8c4ebf..06974fe5 100644 --- a/router.go +++ b/router.go @@ -34,7 +34,7 @@ // 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 // request to that function. -// For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to +// For the methods GET, POST, PUT, PATCH, DELETE and OPTIONS shortcut functions exist to // register handles, for all other methods router.Handle can be used. // // The registered path, against which the router matches incoming requests, can @@ -77,20 +77,21 @@ package httprouter import ( - "io" - "os" - - "fmt" "context" "net/http" "strings" + "sync" + + "fmt" + "io" + "os" "reflect" "runtime" - ) +) // Handle is a function that can be registered to a route to handle HTTP // requests. Like http.HandlerFunc, but has a third parameter for the values of -// wildcards (variables). +// wildcards (path variables). type Handle func(http.ResponseWriter, *http.Request, Params) var DefaultWriter io.Writer = os.Stdout @@ -109,9 +110,9 @@ 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 + for _, p := range ps { + if p.Key == name { + return p.Value } } return "" @@ -129,16 +130,36 @@ func ParamsFromContext(ctx context.Context) Params { return p } +// MatchedRoutePathParam is the Param name under which the path of the matched +// route is stored, if Router.SaveMatchedRoutePath is set. +var MatchedRoutePathParam = "$matchedRoutePath" + +// MatchedRoutePath retrieves the path of the matched route. +// Router.SaveMatchedRoutePath must have been enabled when the respective +// handler was added, otherwise this function always returns an empty string. +func (ps Params) MatchedRoutePath() string { + return ps.ByName(MatchedRoutePathParam) +} + // Router is a http.Handler which can be used to dispatch requests to different // handler functions via configurable routes type Router struct { trees map[string]*node + paramsPool sync.Pool + maxParams uint16 + + // If enabled, adds the matched route path onto the http.Request context + // before invoking the handler. + // The matched route path is only added to handlers of routes that were + // registered when this option was enabled. + SaveMatchedRoutePath bool + // 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. + // and 308 for all other request methods. RedirectTrailingSlash bool // If enabled, the router tries to fix the current request path, if no @@ -146,7 +167,7 @@ type Router struct { // 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 + // to the corrected path with status code 301 for GET requests and 308 for // all other request methods. // For example /FOO and /..//Foo could be redirected to /foo. // RedirectTrailingSlash is independent of this option. @@ -206,8 +227,42 @@ func New() *Router { } } +func (r *Router) getParams() *Params { + ps, _ := r.paramsPool.Get().(*Params) + *ps = (*ps)[0:0] // reset slice + return ps +} + +func (r *Router) putParams(ps *Params) { + if ps != nil { + r.paramsPool.Put(ps) + } +} + +// If enabled, adds the matched route path onto the http.Request context +// before invoking the handler. +// The matched route path is only added to handlers of routes that were +// registered when this option was enabled. +func (r *Router) saveMatchedRoutePath(path string, handles ...Handle) HandlersChain { + for i, handle := range handles { + handles[i] = func(w http.ResponseWriter, req *http.Request, ps Params) { + if ps == nil { + psp := r.getParams() + ps = (*psp)[0:1] + ps[0] = Param{Key: MatchedRoutePathParam, Value: path} + handle(w, req, ps) + r.putParams(psp) + } else { + ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path}) + handle(w, req, ps) + } + } + } + return handles; +} + // GET is a shortcut for router.Handle(http.MethodGet, path, handle) -func (r *Router) GET(path string, handle ...Handle) { +func (r *Router) GET(path string, handle ...Handle) { r.Handle(http.MethodGet, path, handle...) } @@ -241,12 +296,6 @@ func (r *Router) DELETE(path string, handle ...Handle) { r.Handle(http.MethodDelete, path, handle...) } -func If(condition bool, trueVal, falseVal interface{}) interface{} { - if condition { - return trueVal - } - return falseVal -} // Handle registers a new request handle with the given path and method. // // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut @@ -256,25 +305,34 @@ func If(condition bool, trueVal, falseVal interface{}) interface{} { // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). func (r *Router) Handle(method, path string, handles ...Handle) { - //for _, handle := range handles + varsCount := uint16(0) + + if method == "" { + panic("method must not be empty") + } if len(path) < 1 || path[0] != '/' { panic("path must begin with '/' in path '" + path + "'") } + if handles == nil { + panic("handle must not be nil") + } + + if r.SaveMatchedRoutePath { + varsCount++ + handles = r.saveMatchedRoutePath(path, handles...) + } if r.trees == nil { r.trees = make(map[string]*node) } - - //fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> %s (%d handlers)\n", method, path, handlerName, nuHandlers) + //log handles handleNames := "" for _, handle := range handles { - //handleNames = If(len(handleNames) > 0 ? handleNames+",","")+runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() if len(handleNames) > 0 { handleNames = handleNames + "," + runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() }else{ handleNames = runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() } - //runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() } fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> [%s] (%d handlers)\n", method, path,handleNames,len(handles) ) @@ -285,7 +343,21 @@ func (r *Router) Handle(method, path string, handles ...Handle) { r.globalAllowed = r.allowed("*", "") } + root.addRoute(path, handles) + + // Update maxParams + if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams { + r.maxParams = paramsCount + varsCount + } + + // Lazy-init paramsPool alloc func + if r.paramsPool.New == nil && r.maxParams > 0 { + r.paramsPool.New = func() interface{} { + ps := make(Params, 0, r.maxParams) + return &ps + } + } } // Handler is an adapter which allows the usage of an http.Handler as a @@ -346,7 +418,15 @@ func (r *Router) recv(w http.ResponseWriter, req *http.Request) { // the same path with an extra / without the trailing slash should be performed. func (r *Router) Lookup(method, path string) (HandlersChain, Params, bool) { if root := r.trees[method]; root != nil { - return root.getValue(path) + handles, ps, tsr := root.getValue(path, r.getParams) + if handles == nil { + r.putParams(ps) + return nil, nil, tsr + } + if ps == nil { + return handles, nil, tsr + } + return handles, *ps, tsr } return nil, nil, false } @@ -374,7 +454,7 @@ func (r *Router) allowed(path, reqMethod string) (allow string) { continue } - handle, _, _ := r.trees[method].getValue(path) + handle, _, _ := r.trees[method].getValue(path, nil) if handle != nil { // Add request method to list of allowed methods allowed = append(allowed, method) @@ -398,7 +478,8 @@ func (r *Router) allowed(path, reqMethod string) (allow string) { // return as comma separated list return strings.Join(allowed, ", ") } - return + + return allow } // ServeHTTP makes the router implement the http.Handler interface. @@ -412,21 +493,28 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { reqLog := fmt.Sprintf("[http-router] %-6s req.uri=%-12s", req.Method,req.RequestURI) if root := r.trees[req.Method]; root != nil { - if handles, ps, tsr := root.getValue(path); handles != nil { - for _,handle := range handles { - //输出请求的处理日志 - fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) - handle(w, req, ps) + if handles, ps, tsr := root.getValue(path, r.getParams); handles != nil { + if ps != nil { + for _,handle := range handles { + //输出请求的处理日志 + fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) + handle(w, req, *ps) + r.putParams(ps) + } + } else { + for _,handle := range handles { + //输出请求的处理日志 + fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) + handle(w, req, nil) + } } return } else if req.Method != http.MethodConnect && path != "/" { - fmt.Fprintf(DefaultWriter, reqLog) - - code := 301 // Permanent redirect, request with GET method + // Moved Permanently, request with GET method + code := http.StatusMovedPermanently if req.Method != http.MethodGet { - // Temporary redirect, request with same method - // As of Go 1.3, Go does not support status code 308. - code = 307 + // Permanent Redirect, request with same method + code = http.StatusPermanentRedirect } if tsr && r.RedirectTrailingSlash { @@ -446,14 +534,12 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.RedirectTrailingSlash, ) if found { - req.URL.Path = string(fixedPath) + req.URL.Path = fixedPath http.Redirect(w, req, req.URL.String(), code) return } } } - }else{ - fmt.Fprintf(DefaultWriter, reqLog) } if req.Method == http.MethodOptions && r.HandleOPTIONS { diff --git a/tree.go b/tree.go index 0b741101..5c7cd248 100644 --- a/tree.go +++ b/tree.go @@ -17,21 +17,49 @@ func min(a, b int) int { return b } -const maxParamCount uint8 = ^uint8(0) +func longestCommonPrefix(a, b string) int { + i := 0 + max := min(len(a), len(b)) + for i < max && a[i] == b[i] { + i++ + } + return i +} -func countParams(path string) uint8 { - var n uint - for i := 0; i < len(path); i++ { - if path[i] != ':' && path[i] != '*' { +// Search for a wildcard segment and check the name for invalid characters. +// Returns -1 as index, if no wildcard was found. +func findWildcard(path string) (wilcard string, i int, valid bool) { + // Find start + for start, c := range []byte(path) { + // A wildcard starts with ':' (param) or '*' (catch-all) + if c != ':' && c != '*' { continue } - n++ - } - if n >= uint(maxParamCount) { - return maxParamCount + + // Find end and check for invalid characters + valid = true + for end, c := range []byte(path[start+1:]) { + switch c { + case '/': + return path[start : start+1+end], start, valid + case ':', '*': + valid = false + } + } + return path[start:], start, valid } + return "", -1, false +} - return uint8(n) +func countParams(path string) uint16 { + var n uint + for i := range []byte(path) { + switch path[i] { + case ':', '*': + n++ + } + } + return uint16(n) } type nodeType uint8 @@ -45,49 +73,35 @@ const ( //增加调用链特性:HandlersChain defines a HandlerFunc array. type HandlersChain []Handle -/** -http://c.biancheng.net/view/5403.html -radix 的节点类型为 *httprouter.node ,为了说明方便,我们留下了目前关心的几个字段: -path:当前节点对应的路径中的字符串 -wildChild:子节点是否为参数节点,即 wildcard node,或者说 :id 这种类型的节点 -nType:当前节点类型,有四个枚举值: 分别为 static/root/param/catchAll。 -static // 非根节点的普通字符串节点 -root // 根节点 -param // 参数节点,例如 :id -catchAll // 通配符节点,例如 *anyway - -indices:子节点索引,当子节点为非参数类型,即本节点的 wildChild 为 false 时,会将每个子节点的首字母放在该索引数组。说是数组,实际上是个 string。 - */ + type node struct { path string + indices string wildChild bool nType nodeType - maxParams uint8 priority uint32 - indices string children []*node - handles HandlersChain + handlers HandlersChain } -// increments priority of the given child and reorders if necessary +// 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 + cs := n.children + cs[pos].priority++ + prio := cs[pos].priority - // adjust position (move to front) + // Adjust position (move to front) newPos := pos - for newPos > 0 && n.children[newPos-1].priority < prio { - // swap node positions - n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] - - newPos-- + for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { + // Swap node positions + cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] } - // build new index char string + // 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' + 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 @@ -95,255 +109,216 @@ func (n *node) incrementChildPrio(pos int) int { // addRoute adds a node with the given handle to the path. // Not concurrency-safe! -func (n *node) addRoute(path string, handles HandlersChain) { +func (n *node) addRoute(path string, handlers HandlersChain) { fullPath := path 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, - nType: static, - indices: n.indices, - children: n.children, - handles: n.handles, - 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 - } - } + // Empty tree + if n.path == "" && n.indices == "" { + n.insertChild(path, fullPath, handlers) + n.nType = root + return + } - n.children = []*node{&child} - // []byte for proper unicode char conversion, see #65 - n.indices = string([]byte{n.path[i]}) - n.path = path[:i] - n.handles = nil - n.wildChild = false +walk: + for { + // 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 := longestCommonPrefix(path, n.path) + + // Split edge + if i < len(n.path) { + child := node{ + path: n.path[i:], + wildChild: n.wildChild, + nType: static, + indices: n.indices, + children: n.children, + handlers: n.handlers, + priority: n.priority - 1, } - // Make new node a child of this node - if i < len(path) { - path = path[i:] + 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 + } - if n.wildChild { - n = n.children[0] - n.priority++ + // Make new node a child of this node + if i < len(path) { + path = path[i:] - // 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)] && - // Adding a child to a catchAll is not possible - n.nType != catchAll && - // Check for longer wildcard, e.g. :name and :names - (len(n.path) >= len(path) || path[len(n.path)] == '/') { - continue walk - } else { - // Wildcard conflict - var pathSeg string - if n.nType == catchAll { - pathSeg = path - } else { - pathSeg = strings.SplitN(path, "/", 2)[0] - } - prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path - panic("'" + pathSeg + - "' in new path '" + fullPath + - "' conflicts with existing wildcard '" + n.path + - "' in existing prefix '" + prefix + - "'") + if n.wildChild { + n = n.children[0] + n.priority++ + + // Check if the wildcard matches + if len(path) >= len(n.path) && n.path == path[:len(n.path)] && + // Adding a child to a catchAll is not possible + n.nType != catchAll && + // Check for longer wildcard, e.g. :name and :names + (len(n.path) >= len(path) || path[len(n.path)] == '/') { + continue walk + } else { + // Wildcard conflict + pathSeg := path + if n.nType != catchAll { + pathSeg = strings.SplitN(pathSeg, "/", 2)[0] } + prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path + panic("'" + pathSeg + + "' in new path '" + fullPath + + "' conflicts with existing wildcard '" + n.path + + "' in existing prefix '" + prefix + + "'") } + } - c := path[0] + idxc := 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 - } - } + // '/' after param + if n.nType == param && idxc == '/' && len(n.children) == 1 { + n = n.children[0] + n.priority++ + 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 + // Check if a child with the next path byte exists + for i, c := range []byte(n.indices) { + if c == idxc { + i = n.incrementChildPrio(i) + n = n.children[i] + continue walk } - n.insertChild(numParams, path, fullPath, handles) - return + } - } else if i == len(path) { // Make node a (in-path) leaf - if n.handles != nil { - panic("a handle is already registered for path '" + fullPath + "'") - } - n.handles = handles + // Otherwise insert it + if idxc != ':' && idxc != '*' { + // []byte for proper unicode char conversion, see #65 + n.indices += string([]byte{idxc}) + child := &node{} + n.children = append(n.children, child) + n.incrementChildPrio(len(n.indices) - 1) + n = child } + n.insertChild(path, fullPath, handlers) return } - } else { // Empty tree - n.insertChild(numParams, path, fullPath, handles) - n.nType = root + + // Otherwise add handle to current node + if n.handlers != nil { + panic("a handle is already registered for path '" + fullPath + "'") + } + n.handlers = handlers + return } } -func (n *node) insertChild(numParams uint8, path, fullPath string, handles HandlersChain) { - var offset int // already handled bytes of the path +func (n *node) insertChild(path, fullPath string, handlers HandlersChain) { + for { + // Find prefix until first wildcard + wildcard, i, valid := findWildcard(path) + if i < 0 { // No wilcard found + break + } - // find prefix until first wildcard (beginning with ':'' or '*'') - for i, max := 0, len(path); numParams > 0; i++ { - c := path[i] - if c != ':' && c != '*' { - continue + // The wildcard name must not contain ':' and '*' + if !valid { + panic("only one wildcard per path segment is allowed, has: '" + + wildcard + "' in path '" + fullPath + "'") } - // 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, has: '" + - path[i:] + "' in path '" + fullPath + "'") - default: - end++ - } + // Check if the wildcard has a name + if len(wildcard) < 2 { + panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") } - // check if this Node existing children which would be + // Check if this node has existing children which would be // unreachable if we insert the wildcard here if len(n.children) > 0 { - panic("wildcard route '" + path[i:end] + + panic("wildcard segment '" + wildcard + "' conflicts with existing children in path '" + fullPath + "'") } - // check if the wildcard has a name - if end-i < 2 { - panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") - } - - if c == ':' { // param - // split path at the beginning of the wildcard + // param + if wildcard[0] == ':' { if i > 0 { - n.path = path[offset:i] - offset = i + // Insert prefix before the current wildcard + n.path = path[:i] + path = path[i:] } + n.wildChild = true child := &node{ - nType: param, - maxParams: numParams, + nType: param, + path: wildcard, } n.children = []*node{child} - n.wildChild = true n = child n.priority++ - numParams-- - // if the path doesn't end with the wildcard, then there + // 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 - + if len(wildcard) < len(path) { + path = path[len(wildcard):] child := &node{ - maxParams: numParams, - priority: 1, + priority: 1, } n.children = []*node{child} n = child + continue } - } else { // catchAll - if end != max || numParams > 1 { - panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") - } - - if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") - } + // Otherwise we're done. Insert the handle in the new leaf + n.handlers = handlers + return + } - // currently fixed width 1 for '/' - i-- - if path[i] != '/' { - panic("no / before catch-all in path '" + fullPath + "'") - } + // catchAll + if i+len(wildcard) != len(path) { + panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") + } - n.path = path[offset:i] + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") + } - // first node: catchAll node with empty path - child := &node{ - wildChild: true, - nType: catchAll, - maxParams: 1, - } - // update maxParams of the parent node - if n.maxParams < 1 { - n.maxParams = 1 - } - n.children = []*node{child} - n.indices = string(path[i]) - n = child - n.priority++ + // Currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all in path '" + fullPath + "'") + } - // second node: node holding the variable - child = &node{ - path: path[i:], - nType: catchAll, - maxParams: 1, - handles: handles, - priority: 1, - } - n.children = []*node{child} + n.path = path[:i] - return + // First node: catchAll node with empty path + child := &node{ + wildChild: true, + nType: catchAll, + } + n.children = []*node{child} + n.indices = string('/') + n = child + n.priority++ + + // Second node: node holding the variable + child = &node{ + path: path[i:], + nType: catchAll, + handlers: handlers, + priority: 1, } + n.children = []*node{child} + + return } - // insert remaining path part and handle to the leaf - n.path = path[offset:] - n.handles = handles + // If no wildcard was found, simply insert the path and handle + n.path = path + n.handlers = handlers } // Returns the handle registered with the given path (key). The values of @@ -351,19 +326,21 @@ func (n *node) insertChild(numParams uint8, path, fullPath string, handles Handl // 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) (handles HandlersChain, p Params, tsr bool) { -walk: // outer loop for walking the tree +func (n *node) getValue(path string, params func() *Params) (handlers HandlersChain, ps *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):] + prefix := n.path + if len(path) > len(prefix) { + if path[:len(prefix)] == prefix { + path = path[len(prefix):] + // If this node does not have a wildcard (param or catchAll) - // child, we can just look up the next child node and continue + // 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] { + idxc := path[0] + for i, c := range []byte(n.indices) { + if c == idxc { n = n.children[i] continue walk } @@ -372,32 +349,35 @@ walk: // outer loop for walking the tree // 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.handles != nil) + tsr = (path == "/" && n.handlers != nil) return - } - // handle wildcard child + // Handle wildcard child n = n.children[0] switch n.nType { case param: - // find param end (either '/' or path end) + // Find param end (either '/' or path end) end := 0 for end < len(path) && path[end] != '/' { end++ } - // save param value - if p == nil { - // lazy allocation - p = make(Params, 0, n.maxParams) + // Save param value + if params != nil { + if ps == nil { + ps = params() + } + // Expand slice within preallocated capacity + i := len(*ps) + *ps = (*ps)[:i+1] + (*ps)[i] = Param{ + Key: n.path[1:], + Value: path[:end], + } } - 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! + // We need to go deeper! if end < len(path) { if len(n.children) > 0 { path = path[end:] @@ -410,42 +390,49 @@ walk: // outer loop for walking the tree return } - if handles = n.handles; handles != nil { + 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.handles != nil) + tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/") } return case catchAll: - // save param value - if p == nil { - // lazy allocation - p = make(Params, 0, n.maxParams) + // Save param value + if params != nil { + if ps == nil { + ps = params() + } + // Expand slice within preallocated capacity + i := len(*ps) + *ps = (*ps)[:i+1] + (*ps)[i] = Param{ + Key: n.path[2:], + Value: path, + } } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[2:] - p[i].Value = path - handles = n.handles + handlers = n.handlers return default: panic("invalid node type") } } - } else if path == n.path { + } else if path == prefix { // We should have reached the node containing the handle. // Check if this node has a handle registered. - if handles = n.handles; handles != nil { + if handlers = n.handlers; handlers != nil { return } + // If there is no handle for this route, but this route has a + // wildcard child, there must be a handle for this path with an + // additional trailing slash if path == "/" && n.wildChild && n.nType != root { tsr = true return @@ -453,23 +440,22 @@ walk: // outer loop for walking the tree // 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] == '/' { + for i, c := range []byte(n.indices) { + if c == '/' { n = n.children[i] - tsr = (len(n.path) == 1 && n.handles != nil) || - (n.nType == catchAll && n.children[0].handles != nil) + 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.handles != nil) + (len(prefix) == len(path)+1 && prefix[len(path)] == '/' && + path == prefix[:len(prefix)-1] && n.handlers != nil) return } } @@ -478,16 +464,27 @@ walk: // outer loop for walking the tree // 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) { - return n.findCaseInsensitivePathRec( +func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) { + const stackBufSize = 128 + + // Use a static sized buffer on the stack in the common case. + // If the path is too long, allocate a buffer on the heap instead. + buf := make([]byte, 0, stackBufSize) + if l := len(path) + 1; l > stackBufSize { + buf = make([]byte, 0, l) + } + + ciPath := n.findCaseInsensitivePathRec( path, - make([]byte, 0, len(path)+1), // preallocate enough memory for new path - [4]byte{}, // empty rune buffer + buf, // Preallocate enough memory for new path + [4]byte{}, // Empty rune buffer fixTrailingSlash, ) + + return string(ciPath), ciPath != nil } -// shift bytes in array by n bytes left +// Shift bytes in array by n bytes left func shiftNRuneBytes(rb [4]byte, n int) [4]byte { switch n { case 0: @@ -503,14 +500,13 @@ func shiftNRuneBytes(rb [4]byte, n int) [4]byte { } } -// recursive case-insensitive lookup function used by n.findCaseInsensitivePath -func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) { +// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath +func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte { npLen := len(n.path) -walk: // outer loop for walking the tree +walk: // Outer loop for walking the tree for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) { - // add common prefix to result - + // Add common prefix to result oldPath := path path = path[npLen:] ciPath = append(ciPath, n.path...) @@ -520,13 +516,14 @@ walk: // outer loop for walking the tree // we can just look up the next child node and continue to walk down // the tree if !n.wildChild { - // skip rune bytes already processed + // Skip rune bytes already processed rb = shiftNRuneBytes(rb, npLen) if rb[0] != 0 { - // old rune not finished - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == rb[0] { + // Old rune not finished + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { // continue with child node n = n.children[i] npLen = len(n.path) @@ -534,12 +531,12 @@ walk: // outer loop for walking the tree } } } else { - // process a new rune + // Process a new rune var rv rune - // find rune start - // runes are up to 4 byte long, - // -4 would definitely be another rune + // Find rune start. + // Runes are up to 4 byte long, + // -4 would definitely be another rune. var off int for max := min(npLen, 3); off < max; off++ { if i := npLen - off; utf8.RuneStart(oldPath[i]) { @@ -549,38 +546,40 @@ walk: // outer loop for walking the tree } } - // calculate lowercase bytes of current rune + // Calculate lowercase bytes of current rune lo := unicode.ToLower(rv) utf8.EncodeRune(rb[:], lo) - // skip already processed bytes + // Skip already processed bytes rb = shiftNRuneBytes(rb, off) - for i := 0; i < len(n.indices); i++ { - // lowercase matches - if n.indices[i] == rb[0] { + idxc := rb[0] + for i, c := range []byte(n.indices) { + // Lowercase matches + if c == idxc { // must use a recursive approach since both the // uppercase byte and the lowercase byte might exist // as an index - if out, found := n.children[i].findCaseInsensitivePathRec( + if out := n.children[i].findCaseInsensitivePathRec( path, ciPath, rb, fixTrailingSlash, - ); found { - return out, true + ); out != nil { + return out } break } } - // if we found no match, the same for the uppercase rune, + // If we found no match, the same for the uppercase rune, // if it differs if up := unicode.ToUpper(rv); up != lo { utf8.EncodeRune(rb[:], up) rb = shiftNRuneBytes(rb, off) - for i, c := 0, rb[0]; i < len(n.indices); i++ { - // uppercase matches - if n.indices[i] == c { - // continue with child node + idxc := rb[0] + for i, c := range []byte(n.indices) { + // Uppercase matches + if c == idxc { + // Continue with child node n = n.children[i] npLen = len(n.path) continue walk @@ -591,52 +590,55 @@ walk: // outer loop for walking the tree // Nothing found. We can recommend to redirect to the same URL // without a trailing slash if a leaf exists for that path - return ciPath, (fixTrailingSlash && path == "/" && n.handles != nil) + if fixTrailingSlash && path == "/" && n.handlers != nil { + return ciPath + } + return nil } 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++ + // Find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ } - // add param value to case insensitive path - ciPath = append(ciPath, path[:k]...) + // Add param value to case insensitive path + ciPath = append(ciPath, path[:end]...) - // we need to go deeper! - if k < len(path) { + // We need to go deeper! + if end < len(path) { if len(n.children) > 0 { - // continue with child node + // Continue with child node n = n.children[0] npLen = len(n.path) - path = path[k:] + path = path[end:] continue } // ... but we can't - if fixTrailingSlash && len(path) == k+1 { - return ciPath, true + if fixTrailingSlash && len(path) == end+1 { + return ciPath } - return ciPath, false + return nil } - if n.handles != nil { - return ciPath, true + if n.handlers != nil { + return ciPath } 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.handles != nil { - return append(ciPath, '/'), true + if n.path == "/" && n.handlers != nil { + return append(ciPath, '/') } } - return ciPath, false + return nil case catchAll: - return append(ciPath, path...), true + return append(ciPath, path...) default: panic("invalid node type") @@ -644,25 +646,25 @@ walk: // outer loop for walking the tree } else { // We should have reached the node containing the handle. // Check if this node has a handle registered. - if n.handles != nil { - return ciPath, true + if n.handlers != nil { + return ciPath } // 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] == '/' { + for i, c := range []byte(n.indices) { + if c == '/' { n = n.children[i] - if (len(n.path) == 1 && n.handles != nil) || - (n.nType == catchAll && n.children[0].handles != nil) { - return append(ciPath, '/'), true + if (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) { + return append(ciPath, '/') } - return ciPath, false + return nil } } } - return ciPath, false + return nil } } @@ -670,12 +672,12 @@ walk: // outer loop for walking the tree // Try to fix the path by adding / removing a trailing slash if fixTrailingSlash { if path == "/" { - return ciPath, true + return ciPath } if len(path)+1 == npLen && n.path[len(path)] == '/' && - strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handles != nil { - return append(ciPath, n.path...), true + strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil { + return append(ciPath, n.path...) } } - return ciPath, false + return nil } From be5fc4aa42d109481d2b509c419c1eebb9a6b488 Mon Sep 17 00:00:00 2001 From: sunnanping <51907821+sunnanping@users.noreply.github.com> Date: Wed, 9 Jun 2021 09:20:35 +0800 Subject: [PATCH 03/22] Update tree.go --- tree.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tree.go b/tree.go index 5c7cd248..0eb3513e 100644 --- a/tree.go +++ b/tree.go @@ -81,7 +81,7 @@ type node struct { nType nodeType priority uint32 children []*node - handlers HandlersChain + handlers HandlersChain } // Increments priority of the given child and reorders if necessary From 60a094b140f06ba00764dc4b2de6b49b302c61cb Mon Sep 17 00:00:00 2001 From: sunnanping <51907821+sunnanping@users.noreply.github.com> Date: Wed, 9 Jun 2021 09:23:30 +0800 Subject: [PATCH 04/22] Update tree.go --- tree.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tree.go b/tree.go index 0eb3513e..8d1bfb9e 100644 --- a/tree.go +++ b/tree.go @@ -71,7 +71,7 @@ const ( catchAll ) -//增加调用链特性:HandlersChain defines a HandlerFunc array. +//HandlersChain defines a HandlerFunc array. type HandlersChain []Handle type node struct { From eab5cea898d468865b15b42d683a89289bc7e007 Mon Sep 17 00:00:00 2001 From: sunnanping <51907821+sunnanping@users.noreply.github.com> Date: Wed, 9 Jun 2021 09:51:10 +0800 Subject: [PATCH 05/22] Update router.go remove Chinese remark --- router.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/router.go b/router.go index 06974fe5..8d058f88 100644 --- a/router.go +++ b/router.go @@ -489,21 +489,21 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { } path := req.URL.Path - //输出请求的处理日志 + //log RequestURI reqLog := fmt.Sprintf("[http-router] %-6s req.uri=%-12s", req.Method,req.RequestURI) if root := r.trees[req.Method]; root != nil { if handles, ps, tsr := root.getValue(path, r.getParams); handles != nil { if ps != nil { for _,handle := range handles { - //输出请求的处理日志 + //log handler.name fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) handle(w, req, *ps) r.putParams(ps) } } else { for _,handle := range handles { - //输出请求的处理日志 + //log handler.name fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) handle(w, req, nil) } From 6cec2ec4a04fbce2b0f9cb51baa371476d93a2b2 Mon Sep 17 00:00:00 2001 From: sunnanping <51907821+sunnanping@users.noreply.github.com> Date: Wed, 9 Jun 2021 10:44:53 +0800 Subject: [PATCH 06/22] Update tree_test.go according to tree.go,modify unit test --- tree_test.go | 53 +++++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/tree_test.go b/tree_test.go index 2209abcd..e39bb6ff 100644 --- a/tree_test.go +++ b/tree_test.go @@ -25,10 +25,12 @@ import ( // Used as a workaround since we can't compare functions or their addresses var fakeHandlerValue string - -func fakeHandler(val string) Handle { - return func(http.ResponseWriter, *http.Request, Params) { - fakeHandlerValue = val +//HandlersChain +func fakeHandler(val string) HandlersChain { + return []Handle{ + func(http.ResponseWriter, *http.Request, Params) { + fakeHandlerValue = val + }, } } @@ -46,29 +48,30 @@ func getParams() *Params { func checkRequests(t *testing.T, tree *node, requests testRequests) { for _, request := range requests { - handler, psp, _ := tree.getValue(request.path, getParams) - - switch { - case handler == nil: - if !request.nilHandler { - t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) + handlers, psp, _ := tree.getValue(request.path, getParams) + for _,handler := range handlers { + switch { + case handler == nil: + if !request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) + } + case request.nilHandler: + t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) + default: + handler(nil, nil, nil) + if fakeHandlerValue != request.route { + t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) + } } - case request.nilHandler: - t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) - default: - handler(nil, nil, nil) - if fakeHandlerValue != request.route { - t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) - } - } - var ps Params - if psp != nil { - ps = *psp - } + var ps Params + if psp != nil { + ps = *psp + } - if !reflect.DeepEqual(ps, request.ps) { - t.Errorf("Params mismatch for route '%s'", request.path) + if !reflect.DeepEqual(ps, request.ps) { + t.Errorf("Params mismatch for route '%s'", request.path) + } } } } @@ -79,7 +82,7 @@ func checkPriorities(t *testing.T, n *node) uint32 { prio += checkPriorities(t, n.children[i]) } - if n.handle != nil { + if n.handlers != nil { prio++ } From e475d8ddb2211236f162e979e0978589ac35c88b Mon Sep 17 00:00:00 2001 From: sunnanping <51907821+sunnanping@users.noreply.github.com> Date: Wed, 9 Jun 2021 10:47:44 +0800 Subject: [PATCH 07/22] Create router_test.go according to router.go,modify unit test --- router_test.go | 53 ++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/router_test.go b/router_test.go index ae7d2435..9b10a199 100644 --- a/router_test.go +++ b/router_test.go @@ -509,9 +509,9 @@ func TestRouterLookup(t *testing.T) { router := New() // try empty router first - handle, _, tsr := router.Lookup(http.MethodGet, "/nope") - if handle != nil { - t.Fatalf("Got handle for unregistered pattern: %v", handle) + handles, _, tsr := router.Lookup(http.MethodGet, "/nope") + if handles != nil { + t.Fatalf("Got handle for unregistered pattern: %v", handles) } if tsr { t.Error("Got wrong TSR recommendation!") @@ -519,15 +519,19 @@ func TestRouterLookup(t *testing.T) { // insert route and try again router.GET("/user/:name", wantHandle) - handle, params, _ := router.Lookup(http.MethodGet, "/user/gopher") - if handle == nil { - t.Fatal("Got no handle!") - } else { - handle(nil, nil, nil) - if !routed { - t.Fatal("Routing failed!") + handles, params, _ := router.Lookup(http.MethodGet, "/user/gopher") + + for _,handle := range handles { + if handle == nil { + t.Fatal("Got no handle!") + } else { + handle(nil, nil, nil) + if !routed { + t.Fatal("Routing failed!") + } } } + if !reflect.DeepEqual(params, wantParams) { t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) } @@ -535,30 +539,33 @@ func TestRouterLookup(t *testing.T) { // route without param router.GET("/user", wantHandle) - handle, params, _ = router.Lookup(http.MethodGet, "/user") - if handle == nil { - t.Fatal("Got no handle!") - } else { - handle(nil, nil, nil) - if !routed { - t.Fatal("Routing failed!") + handles, params, _ = router.Lookup(http.MethodGet, "/user") + for _,handle := range handles { + if handle == nil { + t.Fatal("Got no handle!") + } else { + handle(nil, nil, nil) + if !routed { + t.Fatal("Routing failed!") + } } } + if params != nil { t.Fatalf("Wrong parameter values: want %v, got %v", nil, params) } - handle, _, tsr = router.Lookup(http.MethodGet, "/user/gopher/") - if handle != nil { - t.Fatalf("Got handle for unregistered pattern: %v", handle) + handles, _, tsr = router.Lookup(http.MethodGet, "/user/gopher/") + if handles != nil { + t.Fatalf("Got handle for unregistered pattern: %v", handles) } if !tsr { t.Error("Got no TSR recommendation!") } - handle, _, tsr = router.Lookup(http.MethodGet, "/nope") - if handle != nil { - t.Fatalf("Got handle for unregistered pattern: %v", handle) + handles, _, tsr = router.Lookup(http.MethodGet, "/nope") + if handles != nil { + t.Fatalf("Got handle for unregistered pattern: %v", handles) } if tsr { t.Error("Got wrong TSR recommendation!") From 56abb5e01d80efb1200509d5ff0cc877aedd2869 Mon Sep 17 00:00:00 2001 From: sunnanping <51907821+sunnanping@users.noreply.github.com> Date: Wed, 9 Jun 2021 11:19:53 +0800 Subject: [PATCH 08/22] Update router.go check the coverage of unit test --- router.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/router.go b/router.go index 8d058f88..a169db54 100644 --- a/router.go +++ b/router.go @@ -325,13 +325,14 @@ func (r *Router) Handle(method, path string, handles ...Handle) { if r.trees == nil { r.trees = make(map[string]*node) } - //log handles + handleNames := "" for _, handle := range handles { + handleName := runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() if len(handleNames) > 0 { - handleNames = handleNames + "," + runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() + handleNames = handleNames + "," + handleName }else{ - handleNames = runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() + handleNames = handleName } } fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> [%s] (%d handlers)\n", method, path,handleNames,len(handles) ) From f992bc415733a60c7cb0dea57524c45887454fa2 Mon Sep 17 00:00:00 2001 From: sunnanping <51907821+sunnanping@users.noreply.github.com> Date: Wed, 9 Jun 2021 11:28:35 +0800 Subject: [PATCH 09/22] Create router.go remove the log --- router.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/router.go b/router.go index a169db54..0b2870f0 100644 --- a/router.go +++ b/router.go @@ -326,6 +326,7 @@ func (r *Router) Handle(method, path string, handles ...Handle) { r.trees = make(map[string]*node) } + /* handleNames := "" for _, handle := range handles { handleName := runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() @@ -336,7 +337,7 @@ func (r *Router) Handle(method, path string, handles ...Handle) { } } fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> [%s] (%d handlers)\n", method, path,handleNames,len(handles) ) - + */ root := r.trees[method] if root == nil { root = new(node) From 585e2da1ddfb84845c341bc04244ffc3eb824723 Mon Sep 17 00:00:00 2001 From: sunnanping <51907821+sunnanping@users.noreply.github.com> Date: Wed, 9 Jun 2021 11:33:04 +0800 Subject: [PATCH 10/22] Update router.go --- router.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/router.go b/router.go index 0b2870f0..bb8eb0b6 100644 --- a/router.go +++ b/router.go @@ -313,8 +313,8 @@ func (r *Router) Handle(method, path string, handles ...Handle) { if len(path) < 1 || path[0] != '/' { panic("path must begin with '/' in path '" + path + "'") } - if handles == nil { - panic("handle must not be nil") + if handles == nil || len(handles)==0 { + panic("handles must not be nil") } if r.SaveMatchedRoutePath { From 3069ee1f55317325df8d0498b64d81f0eb4bc27b Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 13:13:02 +0800 Subject: [PATCH 11/22] update --- router.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/router.go b/router.go index bb8eb0b6..ea19d629 100644 --- a/router.go +++ b/router.go @@ -326,7 +326,7 @@ func (r *Router) Handle(method, path string, handles ...Handle) { r.trees = make(map[string]*node) } - /* + handleNames := "" for _, handle := range handles { handleName := runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() @@ -337,7 +337,7 @@ func (r *Router) Handle(method, path string, handles ...Handle) { } } fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> [%s] (%d handlers)\n", method, path,handleNames,len(handles) ) - */ + root := r.trees[method] if root == nil { root = new(node) From 7422f78a61eed0af0ca6e063707d8352bc9df4e7 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 13:41:49 +0800 Subject: [PATCH 12/22] update --- router_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/router_test.go b/router_test.go index 9b10a199..07ccca6c 100644 --- a/router_test.go +++ b/router_test.go @@ -81,6 +81,15 @@ func TestRouterAPI(t *testing.T) { httpHandler := handlerStruct{&handler} router := New() + router.GET("/GET", []Handle{}) + router.GET("/GET", + func(w http.ResponseWriter, r *http.Request, _ Params) { + get = true + }, + func(w http.ResponseWriter, r *http.Request, _ Params) { + get = true + }, + ) router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true }) From 7c8ed6f99a00de983d0cde05693a9b19f9341a60 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 13:48:20 +0800 Subject: [PATCH 13/22] update --- router_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/router_test.go b/router_test.go index 07ccca6c..dc8f4c1d 100644 --- a/router_test.go +++ b/router_test.go @@ -83,12 +83,9 @@ func TestRouterAPI(t *testing.T) { router := New() router.GET("/GET", []Handle{}) router.GET("/GET", - func(w http.ResponseWriter, r *http.Request, _ Params) { - get = true - }, - func(w http.ResponseWriter, r *http.Request, _ Params) { + func(w http.ResponseWriter, r *http.Request, _ Params) { get = true - }, + },nil, ) router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true From 70be61e94dc647d98de893e103acce7ea2895400 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 15:31:43 +0800 Subject: [PATCH 14/22] update --- router_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/router_test.go b/router_test.go index dc8f4c1d..6b5f4a77 100644 --- a/router_test.go +++ b/router_test.go @@ -82,11 +82,6 @@ func TestRouterAPI(t *testing.T) { router := New() router.GET("/GET", []Handle{}) - router.GET("/GET", - func(w http.ResponseWriter, r *http.Request, _ Params) { - get = true - },nil, - ) router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true }) From b56de680f25b13f5f0c4dd4f9c9a32606505c1f2 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 15:38:21 +0800 Subject: [PATCH 15/22] update --- router_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/router_test.go b/router_test.go index 6b5f4a77..9b10a199 100644 --- a/router_test.go +++ b/router_test.go @@ -81,7 +81,6 @@ func TestRouterAPI(t *testing.T) { httpHandler := handlerStruct{&handler} router := New() - router.GET("/GET", []Handle{}) router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true }) From 521203bbec2b70a868d82c3c15eecfb15bcaf8aa Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 15:44:29 +0800 Subject: [PATCH 16/22] update --- router_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/router_test.go b/router_test.go index 9b10a199..1a8e9b0f 100644 --- a/router_test.go +++ b/router_test.go @@ -81,6 +81,7 @@ func TestRouterAPI(t *testing.T) { httpHandler := handlerStruct{&handler} router := New() + router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true }) From 427d6021202b48313384d7400348cc10a1eb98e7 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 15:53:12 +0800 Subject: [PATCH 17/22] update --- router_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/router_test.go b/router_test.go index 1a8e9b0f..9a92bfc3 100644 --- a/router_test.go +++ b/router_test.go @@ -81,7 +81,6 @@ func TestRouterAPI(t *testing.T) { httpHandler := handlerStruct{&handler} router := New() - router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true }) @@ -551,7 +550,7 @@ func TestRouterLookup(t *testing.T) { } } } - + if params != nil { t.Fatalf("Wrong parameter values: want %v, got %v", nil, params) } From fdc24067bc7077d94e0a514dfb900e25f4cd8c53 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 17:00:48 +0800 Subject: [PATCH 18/22] update --- router.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/router.go b/router.go index ea19d629..7e3d6c7a 100644 --- a/router.go +++ b/router.go @@ -313,8 +313,9 @@ func (r *Router) Handle(method, path string, handles ...Handle) { if len(path) < 1 || path[0] != '/' { panic("path must begin with '/' in path '" + path + "'") } - if handles == nil || len(handles)==0 { - panic("handles must not be nil") + //HandlersChain is nil or array[0] is nil + if handles == nil || (len(handles)==1 && handles[0]==nil){ + panic("handle must not be nil") } if r.SaveMatchedRoutePath { @@ -325,19 +326,18 @@ func (r *Router) Handle(method, path string, handles ...Handle) { if r.trees == nil { r.trees = make(map[string]*node) } - handleNames := "" for _, handle := range handles { - handleName := runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() if len(handleNames) > 0 { - handleNames = handleNames + "," + handleName + handleNames = handleNames + "," + runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() }else{ - handleNames = handleName + handleNames = runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() } } fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> [%s] (%d handlers)\n", method, path,handleNames,len(handles) ) + root := r.trees[method] if root == nil { root = new(node) @@ -491,23 +491,28 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { } path := req.URL.Path - //log RequestURI + //输出请求的处理日志 reqLog := fmt.Sprintf("[http-router] %-6s req.uri=%-12s", req.Method,req.RequestURI) if root := r.trees[req.Method]; root != nil { if handles, ps, tsr := root.getValue(path, r.getParams); handles != nil { if ps != nil { for _,handle := range handles { - //log handler.name + //输出请求的处理日志 fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) handle(w, req, *ps) r.putParams(ps) } } else { for _,handle := range handles { - //log handler.name - fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) - handle(w, req, nil) + if handle != nil{ + //输出请求的处理日志 + fmt.Fprintf(DefaultWriter, reqLog+",req.handle=%-40s\n", runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()) + handle(w, req, nil) + }else{ + //输出请求的处理日志 + fmt.Fprintf(DefaultWriter, reqLog+",req.handle is nil,do nothing.\n") + } } } return From d52555633feab121b375e2c1075fae95d4fb89d4 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 19:27:53 +0800 Subject: [PATCH 19/22] update --- router.go | 6 ++---- router_test.go | 8 ++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/router.go b/router.go index 7e3d6c7a..d2f1aa50 100644 --- a/router.go +++ b/router.go @@ -329,10 +329,8 @@ func (r *Router) Handle(method, path string, handles ...Handle) { handleNames := "" for _, handle := range handles { - if len(handleNames) > 0 { - handleNames = handleNames + "," + runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() - }else{ - handleNames = runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() + if handle != nil{ + handleNames = handleNames+","+runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() } } fmt.Fprintf(DefaultWriter, "[router-debug] %-6s %-40s --> [%s] (%d handlers)\n", method, path,handleNames,len(handles) ) diff --git a/router_test.go b/router_test.go index 9a92bfc3..819525bf 100644 --- a/router_test.go +++ b/router_test.go @@ -84,6 +84,14 @@ func TestRouterAPI(t *testing.T) { router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true }) + router.GET("/GET", + func(w http.ResponseWriter, r *http.Request, _ Params) { + get = true + },func(w http.ResponseWriter, r *http.Request, _ Params) { + get = true + }, + nil, + ) router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { head = true }) From 776a5b9abd3dcf622b2c2111bea572ba790ce363 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 19:32:10 +0800 Subject: [PATCH 20/22] update --- router_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/router_test.go b/router_test.go index 819525bf..21a532d0 100644 --- a/router_test.go +++ b/router_test.go @@ -90,7 +90,6 @@ func TestRouterAPI(t *testing.T) { },func(w http.ResponseWriter, r *http.Request, _ Params) { get = true }, - nil, ) router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { head = true From 9232bd4eaac3022769dcf8a2d9128cb06ae19ce2 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 19:40:46 +0800 Subject: [PATCH 21/22] update --- router_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/router_test.go b/router_test.go index 21a532d0..6a973f00 100644 --- a/router_test.go +++ b/router_test.go @@ -81,9 +81,7 @@ func TestRouterAPI(t *testing.T) { httpHandler := handlerStruct{&handler} router := New() - router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { - get = true - }) + router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true From 603f83478e0bf6794ffeedba3df378dcce259749 Mon Sep 17 00:00:00 2001 From: sunnp Date: Wed, 9 Jun 2021 19:44:59 +0800 Subject: [PATCH 22/22] update --- router_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/router_test.go b/router_test.go index 6a973f00..6d22e251 100644 --- a/router_test.go +++ b/router_test.go @@ -81,13 +81,13 @@ func TestRouterAPI(t *testing.T) { httpHandler := handlerStruct{&handler} router := New() - + router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { get = true },func(w http.ResponseWriter, r *http.Request, _ Params) { get = true - }, + },nil, ) router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { head = true