GIN is a web framework commonly used by golang. It is API-friendly, and the source code comments are also very clear. It is fast and flexible to use, and has a very high fault tolerance rate. The route in the title can be simply understood as the page address entered in the browser, and the "tree" is an optimized data structure. Because the routing tree in the GIN web framework is a prefix tree, we will explain around the prefix tree today.
What is a prefix tree
The prefix tree is actually the Tire tree, a variant of the hash tree, which is usually called a word search tree. Prefix trees are mostly used for statistics, sorting and storing large numbers of strings. Because prefix trees can take advantage of the common prefixes of strings to reduce query time and minimize unnecessary string comparisons. Therefore, prefix trees are often used by search engine systems for text word frequency statistics. A prefix tree has the following characteristics:
- The root node contains no characters, all other nodes contain characters
- The node content of each layer is different
- From the root node to a certain node, the characters passing on the path are connected, which is the string corresponding to the node
- The child nodes of each node usually have a flag bit to mark the end of the word
Taking the Xinhua dictionary as an example when I was a child, let's intuitively understand the prefix tree. I believe that everyone has used the search method of the sequence search method. The operation content is as follows:
- Read the pronunciation of the word accurately, and determine which letter should be looked up according to the syllable of the word.
- Find the letter in the "Chinese Pinyin Syllable Index", find the syllable of the word in the corresponding part of the letter, and read the page number next to the syllable.
- Open the text of the dictionary according to this page number, and find the word you want to look up in the order of four sounds.
This entire process can actually be regarded as a rough prefix tree search process . For example, to find the word "Xinxiang" in the idiom "Xinxiangshicheng", the dictionary has the following structure:
In the process of searching, we find the common part of xi in x according to the first letter x, and then find the corresponding remaining part according to different letters. Put it on the prefix tree search, "heart" in the case corresponds to xi -> n, and "thinking" corresponds to xi -> ang
Prefix Trees in GIN - Compact Prefix Trees
Compared with ordinary prefix trees, the prefix tree in GIN reduces the query level. For example, in the "thinking" we want to find above, where xi is a common part, it can actually be allocated to the same node at the same level instead of divided into two parts. in two parts:
This is the compact prefix tree. Similarly, if we have the following four routes, the compact prefix tree formed by them will look like this:
r.GET("/", handle1)
r.GET("/product", handle2)
r.GET("/product/:id", handle3)
r.GET("/product/:name", handle4)
Store information in nodes
As can be seen from the above content, the address of the entire query in the prefix tree in GIN can be obtained only by splicing each node in the routing tree. So how does GIN complete the addition of these nodes, and what content is stored in each node? We can get the answer to this question through the source code of GIN.
First of all, the common way of declaring routes in GIN is as follows:
func main(){
r := gin.Default()
r.GET("/", func(context *gin.Context) {
context.JSON(200, gin.H{
"status":"ok",
})
})
r.Run()
}
// default会初始化一个engin实例
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
type Engine struct {
RouterGroup
// type RouterGroup struct {
// Handlers HandlersChain
// basePath string
// engine *Engine
// root bool
// }
// 小写私有的,不开放
trees methodTrees
// ...
}
type methodTrees []methodTree
type methodTree struct {
method string
root *node
}
// trees 路由树这一部分由一个带有method 和root字段的node列表维护
// 每个node代表了路由树中的每一个节点
// node所具有的字段内容如下
type node struct {
path string // 当前节点的绝对路径
indices string // 缓存下一节点的第一个字符 在遇到子节点为通配符类型的情况下,indices=''
// 默认是 false,当 children 是 通配符类型时,wildChild 为 true 即 indices=''
wildChild bool // 默认是 false,当 children 是 通配符类型时,wildChild 为 true
// 节点的类型,因为在通配符的场景下在查询的时候需要特殊处理,
// 默认是static类型
// 根节点为 root类型
// 对于 path 包含冒号通配符的情况,nType 是 param 类型
// 对于包含 * 通配符的情况,nType 类型是 catchAll 类型
nType nodeType
// 代表了有几条路由会经过此节点,用于在节点
priority uint32
// 子节点列表
children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain
// 是从 root 节点到当前节点的全部 path 部分;如果此节点为终结节点 handlers 为对应的处理链,否则为 nil;
// maxParams 是当前节点到各个叶子节点的包含的通配符的最大数量
fullPath string
}
// 具体节点类型如下
const (
static nodeType = iota // default, 静态节点,普通匹配(/user)
root // 根节点 (/)
param // 参数节点(/user/:id)
catchAll // 通用匹配,匹配任意参数(*user)
)
To add a route, you can do the following:
// 在创建路由的过程中, 每一个方法都会最终都会被解析后丢给handle函数去处理
func main(){
r := gin.Default()
r.GET("/", func(context *gin.Context) {
context.JSON(200, gin.H{
"status":"ok",
})
})
r.Run()
}
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
// handle函数中会将绝对路径转换为相对路径
// 并将 请求方法、相对路径、处理方法 传给addRoute
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
// 路由的添加主要在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")
// 如果开启了gin的debug模式,则对应处理
debugPrintRoute(method, path, handlers)
// 根据请求方式获取对应的树的根
// 每一个请求方法都有自己对应的一颗紧凑前缀树,这里通过请求方法拿到最顶部的根
root := engine.trees.get(method)
// 如果根为空,则表示这是第一个路由,则自己创建一个以 / 为path的根节点
if root == nil {
// 如果没有就创建
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
// 此处的path是子路由
// 以上内容是做了一层预校验,避免书写不规范导致的请求查询不到
// 接下来是添加路由的正文
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
// 添加完成后,经过此节点的路由条数将会+1
n.priority++
// Empty tree
// 如果为空树, 即只有一个根节点"/" 则插入一个子节点, 并将当前节点设置为root类型的节点
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
parentFullPathIndex := 0
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位置 path[i] == n.path[i]
i := longestCommonPrefix(path, n.path)
// Split edge
// 假设当前节点存在的前缀信息为 hello
// 现有前缀信息为heo的结点进入, 则当前节点需要被拆分
// 拆分成为 he节点 以及 (llo 和 o 两个子节点)
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,
}
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
n.indices = bytesconv.BytesToString([]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
// 将新来的节点插入新的parent节点作为子节点
if i < len(path) {
path = path[i:]
c := path[0]
// '/' after param
// 如果是参数节点 形如/:i
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, max := 0, len(n.indices); i < max; 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 != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65
n.indices += bytesconv.BytesToString([]byte{c})
child := &node{
fullPath: fullPath,
}
n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
} else if n.wildChild {
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
n = n.children[len(n.children)-1]
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
}
// 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 +
"'")
}
n.insertChild(path, fullPath, handlers)
return
}
// Otherwise add handle to current node
// 设置处理函数,如果已经存在,则报错
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
n.fullPath = fullPath
return
}
}
Priority priority
In order to quickly find and combine complete routes, GIN adds the Priority attribute to each node when adding routes. Sort according to Priority when searching. Common nodes (nodes with the most number of passes theoretically) are at the top, and the larger the Priority value in the same level, the higher the priority for matching.
Why put 9 request methods in slice instead of map
This is because 9 request methods correspond to 9 routing trees, and all request methods corresponding to GIN maintain a routing tree. At the same time, these key information are wrapped in the Node structure and placed in an array instead of a map middle. This is to fix the number of requests, and the request method will be maintained in memory after the project is started, and a fixed-length slice is used to reduce memory usage while ensuring a certain query efficiency.
type methodTrees []methodTree
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
Find a route
After the routing tree is built, GIN can start receiving requests normally. The first step is to resolve the routing address from ServeHTTP, and the processing logic of the search process is as follows:
- Allocate a block of memory to fill the response body
- Process request information
- Traverse and compare request methods from trees, and get the routing tree that most corresponds to the request method
- get root node
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
// 真正开始处理请求
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
func (engine *Engine) handleHTTPRequest(c *Context) {
// ...
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
// 根据请求方法进行判断
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 在该方法树上查找路由
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
// 执行处理函数
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next() // 涉及到gin的中间件机制
// 到这里时,请求已经处理完毕,返回的结果也存储在对应的结构体中了
c.writermem.WriteHeaderNow()
return
}
// ...
break
}
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
}
The above is some experience sharing about GIN routing tree, I hope it can help you.
Recommended reading
Interviewer asked: Is parameter passing in Go pass-by-value or pass-by-reference?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。