Gin 的路由是一棵压缩前缀树,但“压缩”之后树到底长什么样?这篇在 Gin 源码副本里加了一个只读的打印函数,把 8 条路由注册后的真实节点逐个打出来,再对着 tree.go 里的字段和注释看每个字段是干什么的。
tree.go(Gin v1.12.0):
90 | type nodeType uint8
91 |
92 | const (
93 | static nodeType = iota
94 | root
95 | param
96 | catchAll
97 | )
98 |
99 | type node struct {
100 | path string
101 | indices string
102 | wildChild bool
103 | nType nodeType
104 | priority uint32
105 | children []*node // child nodes, at most 1 :param style node at the end of the array
106 | handlers HandlersChain
107 | fullPath string
108 | }Gin 的路由树节点非常小:一条边上的字符串 path、子节点首字节拼成的 indices、是否带通配子节点的 wildChild、类型 nType、优先级 priority、children、handlers 和完整路由 fullPath。类型只有 static、root、param、catchAll 四种,root 不是一种通配语法,只是根节点的标记。
children 的源码注释写着“最多一个 :param 风格的节点,放在数组最后”,下一节的树印证了这点。
注册顺序: [/ /users /users/:id /users/:id/posts /users/new /static/*filepath /search /src/a/b]
"/" [root prio=8 indices="us"] handlers=1 full="/"
"users" [static prio=4 indices="/"] handlers=1 full="/users"
"/" [static prio=3 indices="n" wildChild]
"new" [static prio=1 indices=""] handlers=1 full="/users/new"
":id" [param prio=2 indices="/"] handlers=1 full="/users/:id"
"/posts" [static prio=1 indices=""] handlers=1 full="/users/:id/posts"
"s" [static prio=3 indices="ter"]
"tatic" [static prio=1 indices="/"]
"" [catchAll prio=1 indices="" wildChild]
"/*filepath" [catchAll prio=1 indices=""] handlers=1 full="/static/*filepath"
"earch" [static prio=1 indices=""] handlers=1 full="/search"
"rc/a/b" [static prio=1 indices=""] handlers=1 full="/src/a/b"“压缩前缀树”的压缩体现在:/users、/users/:id、/users/new 共用 users 这条边,/static、/search、/src/a/b 共用首字母 s。indices 只放静态子节点的首字节,所以 :id 不在 "/users/" 这层的 indices 里(那里只有 n),而是排在 children 数组的最后,wildChild 为真。
树里“长什么样”我只验证到这一层;匹配时先查 indices、再落到通配子节点的顺序,在后面讲优先级和 param 与 catchAll 的差别时再用实验验证。
[只有 /users/:id]
"/users/" [root prio=1 indices="" wildChild]
":id" [param prio=1 indices=""] handlers=1 full="/users/:id"
[只有 /static/*filepath]
"/static" [root prio=1 indices="/"]
"" [catchAll prio=1 indices="" wildChild]
"/*filepath" [catchAll prio=1 indices=""] handlers=1 full="/static/*filepath"
[/a/:x/b/:y/*rest]
"/a/" [root prio=1 indices="" wildChild]
":x" [param prio=1 indices=""]
"/b/" [static prio=1 indices="" wildChild]
":y" [param prio=1 indices=""]
"" [static prio=1 indices="/"]
"" [catchAll prio=1 indices="" wildChild]
"/*rest" [catchAll prio=1 indices=""] handlers=1 full="/a/:x/b/:y/*rest"只有 /users/:id 时,树就是一个 "/users/" 带一个 ":id"。*filepath 则占两个节点:一个 path 为空的 catchAll 节点和下面的 "/*filepath"。我只验证了树的形状,没有逐行去验证 insertChild 里每个分支的作用。
/users/new → FullPath="/users/new" Params=[]
/users/42 → FullPath="/users/:id" Params=[{id 42}]
/users/42/posts → FullPath="/users/:id/posts" Params=[{id 42}]
/static/css/app.css → FullPath="/static/*filepath" Params=[{filepath /css/app.css}]
/static/ → FullPath="/static/*filepath" Params=[{filepath /}]
/a/1/b/2/c/d/e → FullPath="/a/:x/b/:y/*rest" Params=[{x 1} {y 2} {rest /c/d/e}]
/users/ → 状态码 404 位置 ""
/users/42/post → 状态码 404 位置 ""
/static → 状态码 301 位置 "/static/"c.FullPath() 返回的是注册时的路由模板,c.Params 才是 URL 里取出的值。通配符 *filepath 的值总是以 / 开头,拼路径时别再多加一个斜杠。没有注册 /users 时,/users/ 是 404;/static 因为后面有 /static/*filepath,被 301 重定向到 /static/。
1000 条静态路由 节点数: 1100 1000 条带 :id 的路由 节点数: 2100 4 个方法各注册 /x 之后的树(顺序): [GET POST PUT DELETE]
静态路由共用前缀,1000 条只要 1100 个节点;每条路由多一个参数就多一个 param 节点,2100 个。不同 HTTP 方法各自一棵树,所以同一个路径用 GET 和 POST 注册不会互相干扰,树也不会共享。