:id 和 *path 看起来只是“少一段”和“多一段”的区别,但它们的节点类型、冲突规则、值的格式都不一样。这篇用同一批请求打在两种路由上,再把能不能共存的十种组合逐个注册一遍,把 panic 原文列出来。
GET /u/1 → 200 "id=\"1\""
GET /u/1/2 → 404 "404 page not found"
GET /u/ → 404 "404 page not found"
GET /u → 404 "404 page not found"
GET /u/a%20b → 200 "id=\"a b\""
GET /u/中文 → 200 "id=\"中文\""
GET /s/ → 200 "p=\"/\""
GET /s/a → 200 "p=\"/a\""
GET /s/a/b/c → 200 "p=\"/a/b/c\""
GET /s/a/b/ → 200 "p=\"/a/b/\""
GET /s → 301 "<a href=\"/s/\">Moved Permanently</a>.\n\n" Location=/s/
GET /s//x → 200 "p=\"//x\"":id 只匹配到下一个斜杠为止,而且不能为空:/u/1/2、/u/ 都是 404。*p 把斜杠之后的所有内容当成一个值,哪怕只是一个 /。URL 里的 %20 和中文在取值时已经解码。
注意 /s 没有匹配而是被 301 重定向到 /s/:*p 前面的斜杠是路由的一部分。
[:id 与 *p 同位置] panic: '*p' in new path '/x/*p' conflicts with existing wildcard ':id' in existing prefix '/x/:id'
[*p 与 :id 同位置(顺序反过来)] panic: '/:id' in new path '/x/:id' conflicts with existing wildcard '/*p' in existing prefix '/x/*p'
[:id 与 :name 同位置] panic: ':name' in new path '/x/:name' conflicts with existing wildcard ':id' in existing prefix '/x/:id'
[:id 与 :name 后面还有静态段] panic: ':name' in new path '/x/:name/b' conflicts with existing wildcard ':id' in existing prefix '/x/:id'
[同名 :id 后面不同的静态段] 注册成功: [/x/:id/a /x/:id/b]
[*p 之后还有路径] panic: catch-all routes are only allowed at the end of the path in path '/x/*p/more'
[*p 与静态 /x/ 同位置] panic: '/' in new path '/x/' conflicts with existing wildcard '/*p' in existing prefix '/*p'
[*p 与静态 /x/a 同位置] panic: '/a' in new path '/x/a' conflicts with existing wildcard '/*p' in existing prefix '/x/*p'
[静态 /x/a 先注册、*p 后注册] panic: catch-all wildcard '*p' in new path '/x/*p' conflicts with existing path segment 'a' in existing prefix '/x/a'
[:id 与静态 /x/new 共存] 注册成功: [/x/new /x/:id]同一位置只能有一种通配符::id 与 *p 不能并存,:id 与 :name 也不行(即使它们后面跟着不同的静态段)。catch-all 只能出现在路径末尾,而且不能与同一位置的静态路径并存,先后顺序都一样。能共存的是:同名的 :id 后面接不同的静态段,以及静态路径与 :id 并存(下一篇会逐个验证)。
GET /files/a/b.txt → 200 "p=\"/a/b.txt\" 去掉开头斜杠=\"a/b.txt\""
GET /files/ → 200 "p=\"/\" 去掉开头斜杠=\"\""
GET /files → 301 "<a href=\"/files/\">Moved Permanently</a>.\n\n" Location=/files/
GET /files/a//b → 200 "p=\"/a//b\" 去掉开头斜杠=\"a//b\""
GET /files/%2e%2e/x → 200 "p=\"/../x\" 去掉开头斜杠=\"../x\""
GET /files/../x → 200 "p=\"/../x\" 去掉开头斜杠=\"../x\""*p 的值总是以 / 开头,所以用它拼文件路径时常写 p[1:],并且要处理空串。/files/../x 和 /files/%2e%2e/x 的 p 都是 "/../x"——Gin 的路由不会清理或拒绝 ..,把它拼进文件路径之前必须自己处理。
GET /files/a.txt → 200 "name=\"a.txt\""
GET /files/dir/a.txt → 404 "404 page not found"
GET /files/dir%2Fa.txt → 404 "404 page not found"
--- UseRawPath=true + UnescapePathValues=true 之后 ---
GET /files/a.txt → 200 "name=\"a.txt\""
GET /files/dir/a.txt → 404 "404 page not found"
GET /files/dir%2Fa.txt → 200 "name=\"dir/a.txt\"":name 永远是一段。默认情况下 Gin 用已经解码的 URL.Path 匹配,%2F 解码成斜杠后就变成了两段,所以 404;把 UseRawPath 和 UnescapePathValues 都设为 true,就改用 RawPath 匹配、再对取出的值解码,这时 dir%2Fa.txt 能命中,值是 dir/a.txt。