brief introduction
Gin source interpretation, based on v1.5.0 Edition.
HttpRouter implementation
Adding routes is mainly done by addRoute:
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { assert1(path[0] == '/', "path must begin with '/'") assert1(method != "", "HTTP method can not be empty") assert1(len(handlers) > 0, "there must be at least one handler") debugPrintRoute(method, path, handlers) root := engine.trees.get(method) if root == nil { root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) }
Gin's route is through httprouter Implemented to learn more about its source code.
data structure
github's documentation explains how this works and is available for reference How does it work?.
HttpRouter uses Radix trees internally, a compact variant of the prefix tree.
The above image, from Wikipedia, shows the structure of the Radix tree. Compared to a common prefix tree, the Radix tree stores more than one character on its edge, greatly compressing its depth.
Take a look at the definition of the data structure:
// Param is a single URL parameter, consisting of a key and a value. type Param struct { Key string Value string } // Params is a Param-slice, as returned by the router. // The slice is ordered, the first URL parameter is also the first slice value. // It is therefore safe to read values by the index. type Params []Param type methodTree struct { method string root *node } type methodTrees []methodTree
The type of Engine.trees is methodTrees, and the initialization statement is trees: make(methodTrees, 0, 9),.
func (trees methodTrees) get(method string) *node { for _, tree := range trees { if tree.method == method { return tree.root } } return nil }
The first step in the previous routing code is to find root, which is root: = engine.trees.get (method), combined with get code.
We can see that methodTrees are actually classified by HTTP methods, each corresponding to a tree.
If the current HTTP method of this type does not exist, create a new tree methodTree:
root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root})
Take another look at how tree nodes are defined:
type nodeType uint8 const ( static nodeType = iota // default root param catchAll ) type node struct { path string indices string children []*node handlers HandlersChain priority uint32 nType nodeType maxParams uint8 wildChild bool fullPath string }
Add Route
Now that you know the data structure, take a look at how routes are added, root.addRoute(path, handlers).
// addRoute adds a node with the given handle to the path. // Not concurrency-safe! func (n *node) addRoute(path string, handlers HandlersChain) { fullPath := path n.priority++ numParams := countParams(path) parentFullPathIndex := 0 // non-empty tree if len(n.path) > 0 || len(n.children) > 0 { walk: for { // Update maxParams of the current node if numParams > n.maxParams { n.maxParams = numParams } // Find the longest common prefix. // This also implies that the common prefix contains no ':' or '*' // since the existing key can't contain those chars. i := 0 max := min(len(path), len(n.path)) for i < max && path[i] == n.path[i] { i++ } // Split edge if i < len(n.path) { child := node{ path: n.path[i:], wildChild: n.wildChild, indices: n.indices, children: n.children, handlers: n.handlers, priority: n.priority - 1, fullPath: n.fullPath, } // Update maxParams (max of all children) for i := range child.children { if child.children[i].maxParams > child.maxParams { child.maxParams = child.children[i].maxParams } } n.children = []*node{&child} // []byte for proper unicode char conversion, see #65 n.indices = string([]byte{n.path[i]}) n.path = path[:i] n.handlers = nil n.wildChild = false n.fullPath = fullPath[:parentFullPathIndex+i] } // Make new node a child of this node if i < len(path) { path = path[i:] if n.wildChild { parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ // Update maxParams of the child node if numParams > n.maxParams { n.maxParams = numParams } numParams-- // Check if the wildcard matches if len(path) >= len(n.path) && n.path == path[:len(n.path)] { // check for longer wildcard, e.g. :name and :names if len(n.path) >= len(path) || path[len(n.path)] == '/' { continue walk } } pathSeg := path if n.nType != catchAll { 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 { parentFullPathIndex += len(n.path) 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] { parentFullPathIndex += len(n.path) 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, fullPath: fullPath, } n.children = append(n.children, child) n.incrementChildPrio(len(n.indices) - 1) n = child } n.insertChild(numParams, path, fullPath, handlers) return } else if i == len(path) { // Make node a (in-path) leaf if n.handlers != nil { panic("handlers are already registered for path '" + fullPath + "'") } n.handlers = handlers } return } } else { // Empty tree n.insertChild(numParams, path, fullPath, handlers) n.nType = root } }
addRoute
The code is a bit long. First, there are two cases based on the if statement, one is when the tree is initialized (that is, the tree is empty), the other is when the tree is not empty.
n.insertChild(numParams, path, fullPath, handlers) n.nType = root
If the tree is empty, that is, n.path is an empty string (initial value) and n.children is an empty slice.
At this point, just insert the node through insertChild and set the node's type to root.
The code for insertChild is a bit long, so come back later.
When the tree is not empty and you enter a for loop, follow the comments to see what the for does in general.
// 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 // Make new node a child of this node
The first two steps, updating maxParams and calculating the length of the longest prefix, are simple, just look at the code.
See how the nodes split, step three:
// Split edge if i < len(n.path) { child := node{ path: n.path[i:], wildChild: n.wildChild, indices: n.indices, children: n.children, handlers: n.handlers, priority: n.priority - 1, fullPath: n.fullPath, } // Update maxParams (max of all children) for i := range child.children { if child.children[i].maxParams > child.maxParams { child.maxParams = child.children[i].maxParams } } n.children = []*node{&child} // []byte for proper unicode char conversion, see #65 n.indices = string([]byte{n.path[i]}) n.path = path[:i] n.handlers = nil n.wildChild = false n.fullPath = fullPath[:parentFullPathIndex+i] }
When the length of the common prefix is less than n.path, the current node splits into a child node.
For example, the current node node.path ='/ping', will split when it encounters path ='/pong',
The length of the common prefix i=2, so the node will split into node.path ='/p'and node.path ='ing'.
The split node will take up most of the attributes of the current node.
Next, look at the fourth step, how to add a child node to the current node, which is the core code of root.addRoute(path, handlers).
This is also an if judgment. Let's look at the second half first, which is what happens when something goes wrong:
else if i == len(path) { // Make node a (in-path) leaf if n.handlers != nil { panic("handlers are already registered for path '" + fullPath + "'") } n.handlers = handlers }
If handlers are not empty, an error will occur, indicating that handlers are only allowed to register once.
Look at the first half of if, when if I < len(path):
// Make new node a child of this node if i < len(path) { path = path[i:] if n.wildChild { parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ // Update maxParams of the child node if numParams > n.maxParams { n.maxParams = numParams } numParams-- // Check if the wildcard matches if len(path) >= len(n.path) && n.path == path[:len(n.path)] { // check for longer wildcard, e.g. :name and :names if len(n.path) >= len(path) || path[len(n.path)] == '/' { continue walk } } pathSeg := path if n.nType != catchAll { 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 { parentFullPathIndex += len(n.path) 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] { parentFullPathIndex += len(n.path) 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, fullPath: fullPath, } n.children = append(n.children, child) n.incrementChildPrio(len(n.indices) - 1) n = child } n.insertChild(numParams, path, fullPath, handlers) return }
This part is also a bit long and needs to be broken down step by step.
First, according to path = path[i:], we find that path has removed the common prefix.
Let's look at the first judgment, if n.wildChild, where there are wildcard child nodes:
if n.wildChild { parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ // Update maxParams of the child node if numParams > n.maxParams { n.maxParams = numParams } numParams-- // Check if the wildcard matches if len(path) >= len(n.path) && n.path == path[:len(n.path)] { // check for longer wildcard, e.g. :name and :names if len(n.path) >= len(path) || path[len(n.path)] == '/' { continue walk } } pathSeg := path if n.nType != catchAll { 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 + "'") }
In the judgment of wildcards, wildcards conflict is usually triggered incorrectly, unless the previous wildcard part is the same, followed by /.
c := path[0] // slash after param if n.nType == param && c == '/' && len(n.children) == 1 { parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ continue walk }
When the node is a wildcard and the path starts/goes into a new round of looping.
// Check if a child with the next path byte exists for i := 0; i < len(n.indices); i++ { if c == n.indices[i] { parentFullPathIndex += len(n.path) i = n.incrementChildPrio(i) n = n.children[i] continue walk } }
Check to see if there is a child node, jump directly to that node if there is one, and enter a new cycle.
When the previous node splits, n.indices = string([]byte{n.path[i]}) is set.
// Otherwise insert it if c != ':' && c != '*' { // []byte for proper unicode char conversion, see #65 n.indices += string([]byte{c}) child := &node{ maxParams: numParams, fullPath: fullPath, } n.children = append(n.children, child) n.incrementChildPrio(len(n.indices) - 1) n = child }
After making the previous judgment, come here and if c is not: or *, insert a node and replace the current node with this one.
n.insertChild(numParams, path, fullPath, handlers) return
Finally, insertChild is still called. Finally, return can be used to jump out of the loop and end the method.
insertChild
n.insertChild(numParams, path, fullPath, handlers) was called in two places above to see its implementation.
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { var offset int // already handled bytes of the path // find prefix until first wildcard (beginning with ':' or '*') for i, max := 0, len(path); numParams > 0; i++ { c := path[i] if c != ':' && c != '*' { continue } // 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 existing children which would be // unreachable if we insert the wildcard here if len(n.children) > 0 { panic("wildcard route '" + path[i:end] + "' 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 if i > 0 { n.path = path[offset:i] offset = i } child := &node{ nType: param, maxParams: numParams, fullPath: fullPath, } n.children = []*node{child} n.wildChild = true n = child n.priority++ numParams-- // if the path doesn't end with the wildcard, then there // will be another non-wildcard subpath starting with '/' if end < max { n.path = path[offset:end] offset = end child := &node{ maxParams: numParams, priority: 1, fullPath: fullPath, } n.children = []*node{child} n = child } } else { // catchAll if end != max || numParams > 1 { panic("catch-all routes are only allowed at the end of the path 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 + "'") } n.path = path[offset:i] // first node: catchAll node with empty path child := &node{ wildChild: true, nType: catchAll, maxParams: 1, fullPath: fullPath, } n.children = []*node{child} n.indices = string(path[i]) n = child n.priority++ // second node: node holding the variable child = &node{ path: path[i:], nType: catchAll, maxParams: 1, handlers: handlers, priority: 1, fullPath: fullPath, } n.children = []*node{child} return } } // insert remaining path part and handle to the leaf n.path = path[offset:] n.handlers = handlers n.fullPath = fullPath }
Collapse the code, mainly in two parts, a for loop, and some statements to update the properties.
// insert remaining path part and handle to the leaf n.path = path[offset:] n.handlers = handlers n.fullPath = fullPath
Let's look mainly at the for loop:
// find prefix until first wildcard (beginning with ':' or '*') for i, max := 0, len(path); numParams > 0; i++ { c := path[i] if c != ':' && c != '*' { continue }
These lines of judgment, as explained in the comment, do not really begin processing until they encounter the wildcard character':'or'*'.
Note that the judgement is numParams, which indicates several wildcard parameters.
// 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++ } }
This is also a judgment to verify that no more than one':'and'*' can appear in a wildcard name.
// check if this Node existing children which would be // unreachable if we insert the wildcard here if len(n.children) > 0 { panic("wildcard route '" + path[i:end] + "' 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 + "'") }
Two more judgments, the first to verify that the current node cannot store child nodes, otherwise the wildcard node will conflict.
The second name used to validate a wildcard node must be at least one character long.
Finally, construct them separately based on the wildcards. First, look at the code when c ==':':
if c == ':' { // param // split path at the beginning of the wildcard if i > 0 { n.path = path[offset:i] offset = i } child := &node{ nType: param, maxParams: numParams, fullPath: fullPath, } n.children = []*node{child} n.wildChild = true n = child n.priority++ numParams-- // if the path doesn't end with the wildcard, then there // will be another non-wildcard subpath starting with '/' if end < max { n.path = path[offset:end] offset = end child := &node{ maxParams: numParams, priority: 1, fullPath: fullPath, } n.children = []*node{child} n = child } }
Then the code for c =='*', which is the else part:
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 + "'") } // currently fixed width 1 for '/' i-- if path[i] != '/' { panic("no / before catch-all in path '" + fullPath + "'") } n.path = path[offset:i] // first node: catchAll node with empty path child := &node{ wildChild: true, nType: catchAll, maxParams: 1, fullPath: fullPath, } n.children = []*node{child} n.indices = string(path[i]) n = child n.priority++ // second node: node holding the variable child = &node{ path: path[i:], nType: catchAll, maxParams: 1, handlers: handlers, priority: 1, fullPath: fullPath, } n.children = []*node{child} return }
The catchAll wildcard is a bit special. No other wildcard parameters are allowed after it, so the first few lines are trying to determine if the requirements are met.
During this process, two nodes of type catchAll are created, and the first node indicates the storage of wildcard child nodes, wildChild=true.
The second node will have specific content.
This is basically the process of adding routes, so let's see how to read the data.
get data
Getting data from a tree occurs mainly in func (engine *Engine) handleHTTPRequest(c *Context).
Take a look at the snippet:
root := t[i].root // Find route in tree value := root.getValue(rPath, c.Params, unescape) if value.handlers != nil { c.handlers = value.handlers c.Params = value.params c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return }
The data is obtained mainly through the getValue method, with the complete code as follows:
// getValue returns the handle registered with the given path (key). The values of // wildcards are saved to a map. // If no handle can be found, a TSR (trailing slash redirect) recommendation is // made if a handle exists with an extra (without the) trailing slash for the // given path. func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) { value.params = po walk: // Outer loop for walking the tree for { if len(path) > len(n.path) { if path[:len(n.path)] == n.path { path = path[len(n.path):] // If this node does not have a wildcard (param or catchAll) // child, we can just look up the next child node and continue // to walk down the tree if !n.wildChild { c := path[0] for i := 0; i < len(n.indices); i++ { if c == n.indices[i] { n = n.children[i] continue walk } } // Nothing found. // We can recommend to redirect to the same URL without a // trailing slash if a leaf exists for that path. value.tsr = path == "/" && n.handlers != nil return } // handle wildcard child n = n.children[0] switch n.nType { case param: // find param end (either '/' or path end) end := 0 for end < len(path) && path[end] != '/' { end++ } // save param value if cap(value.params) < int(n.maxParams) { value.params = make(Params, 0, n.maxParams) } i := len(value.params) value.params = value.params[:i+1] // expand slice within preallocated capacity value.params[i].Key = n.path[1:] val := path[:end] if unescape { var err error if value.params[i].Value, err = url.QueryUnescape(val); err != nil { value.params[i].Value = val // fallback, in case of error } } else { value.params[i].Value = val } // we need to go deeper! if end < len(path) { if len(n.children) > 0 { path = path[end:] n = n.children[0] continue walk } // ... but we can't value.tsr = len(path) == end+1 return } if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath return } 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] value.tsr = n.path == "/" && n.handlers != nil } return case catchAll: // save param value if cap(value.params) < int(n.maxParams) { value.params = make(Params, 0, n.maxParams) } i := len(value.params) value.params = value.params[:i+1] // expand slice within preallocated capacity value.params[i].Key = n.path[2:] if unescape { var err error if value.params[i].Value, err = url.QueryUnescape(path); err != nil { value.params[i].Value = path // fallback, in case of error } } else { value.params[i].Value = path } value.handlers = n.handlers value.fullPath = n.fullPath return default: panic("invalid node type") } } } else if path == n.path { // We should have reached the node containing the handle. // Check if this node has a handle registered. if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath return } if path == "/" && n.wildChild && n.nType != root { value.tsr = true return } // No handle found. Check if a handle for this path + a // trailing slash exists for trailing slash recommendation for i := 0; i < len(n.indices); i++ { if n.indices[i] == '/' { n = n.children[i] value.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 value.tsr = (path == "/") || (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && path == n.path[:len(n.path)-1] && n.handlers != nil) return } }
The code is a bit long. Read the comment first. Get the handlers registered on it, mainly based on the path and parameters.
// nodeValue holds return values of (*Node).getValue method type nodeValue struct { handlers HandlersChain params Params tsr bool fullPath string } // Param is a single URL parameter, consisting of a key and a value. type Param struct { Key string Value string }
The structure used inside is shown above. The main part of the method is a for loop.
Inside the for loop, the first half is a judgment, so let's look at the second half first.
// Nothing found. We can recommend to redirect to the same URL with an // extra trailing slash if a leaf exists for that path value.tsr = (path == "/") || (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && path == n.path[:len(n.path)-1] && n.handlers != nil) return
If no corresponding match is found, an identification called tsr is set to determine whether the TSR (trailing slash redirect), or tail slash redirect, is met. For example, /path can be redirected to/path/.
To return to if judgment, first look at the first judgment section, if len (path) > len (n.path).
if len(path) > len(n.path) { if path[:len(n.path)] == n.path { path = path[len(n.path):] // If this node does not have a wildcard (param or catchAll) // child, we can just look up the next child node and continue // to walk down the tree if !n.wildChild { c := path[0] for i := 0; i < len(n.indices); i++ { if c == n.indices[i] { n = n.children[i] continue walk } } // Nothing found. // We can recommend to redirect to the same URL without a // trailing slash if a leaf exists for that path. value.tsr = path == "/" && n.handlers != nil return } // handle wildcard child n = n.children[0] switch n.nType { case param: // find param end (either '/' or path end) end := 0 for end < len(path) && path[end] != '/' { end++ } // save param value if cap(value.params) < int(n.maxParams) { value.params = make(Params, 0, n.maxParams) } i := len(value.params) value.params = value.params[:i+1] // expand slice within preallocated capacity value.params[i].Key = n.path[1:] val := path[:end] if unescape { var err error if value.params[i].Value, err = url.QueryUnescape(val); err != nil { value.params[i].Value = val // fallback, in case of error } } else { value.params[i].Value = val } // we need to go deeper! if end < len(path) { if len(n.children) > 0 { path = path[end:] n = n.children[0] continue walk } // ... but we can't value.tsr = len(path) == end+1 return } if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath return } 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] value.tsr = n.path == "/" && n.handlers != nil } return case catchAll: // save param value if cap(value.params) < int(n.maxParams) { value.params = make(Params, 0, n.maxParams) } i := len(value.params) value.params = value.params[:i+1] // expand slice within preallocated capacity value.params[i].Key = n.path[2:] if unescape { var err error if value.params[i].Value, err = url.QueryUnescape(path); err != nil { value.params[i].Value = path // fallback, in case of error } } else { value.params[i].Value = path } value.handlers = n.handlers value.fullPath = n.fullPath return default: panic("invalid node type") } } }
This part of the judgment is nested with an if judgment, which is used to determine that the prefix of the path matches the path of the current node, or skip directly if it is not equal.
Then it is based on n.wildChild, that is, on whether there are wildcard child nodes.
If there are no wildcard child nodes, the next child node will continue to be found and a new round of for loops will occur.
If no child node is found, it is returned directly. The existence of a child node is determined by n.indices.
n.indices is a string that holds the first character of all subnode paths.
For example, if two paths are currently registered, /ping and/pong, then the current node is the / p public prefix,
Then it's n.indices="io".
If there are wildcard child nodes, the selection process will be based on the type of n.nType.
If the type is param, even if a named variable is used, the value of that variable will be saved first.
If there is still left if end < len(path) {in the length, it will enter a new for loop;
Denial is considered complete, just copy handlers and fullPath.
If the type is catchAll, it is easier to use any matching variable of the *command.
Because there are no further paths to consider, * matches all remaining paths.
Save the variable values directly, then copy the handlers and fullPath.
A panic is triggered if the type does not match both of the above types.
Then look at another judgment, else if path == n.path.
else if path == n.path { // We should have reached the node containing the handle. // Check if this node has a handle registered. if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath return } if path == "/" && n.wildChild && n.nType != root { value.tsr = true return } // No handle found. Check if a handle for this path + a // trailing slash exists for trailing slash recommendation for i := 0; i < len(n.indices); i++ { if n.indices[i] == '/' { n = n.children[i] value.tsr = (len(n.path) == 1 && n.handlers != nil) || (n.nType == catchAll && n.children[0].handlers != nil) return } } return }
This part of the processing is also relatively simple, similar to the previous logic, mainly to see if there is handler registration on the path.
If no handler is registered, the value.tsr is checked for tail slash redirection.
As a result, the process of getting data from a tree has been completed.
summary
Good code should be read more, which helps you understand the principles and broaden your horizons.
On the other hand, debuggers can be really useful when reading code, especially when observing how data structures are stored.