Group 只是路径前缀加一份中间件的拷贝?这篇逐个验证:路径怎么拼、嵌套组的中间件怎么叠加、什么时候“固化”,以及一个容易忽略的结论——在组里 Use 的中间件管不到这个前缀下的 404。
utils.go(Gin v1.12.0):
135 | func joinPaths(absolutePath, relativePath string) string {
136 | if relativePath == "" {
137 | return absolutePath
138 | }
139 |
140 | finalPath := path.Join(absolutePath, relativePath)
141 | if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
142 | return finalPath + "/"
143 | }
144 | return finalPath
145 | }
routergroup.go 的 Group(Gin v1.12.0):
72 | func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
73 | return &RouterGroup{
74 | Handlers: group.combineHandlers(handlers),
75 | basePath: group.calculateAbsolutePath(relativePath),
76 | engine: group.engine,
77 | }
78 | }
79 |
routergroup.go 的 combineHandlers(Gin v1.12.0):
241 | func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
242 | finalSize := len(group.Handlers) + len(handlers)
243 | assert1(finalSize < int(abortIndex), "too many handlers")
244 | mergedHandlers := make(HandlersChain, finalSize)
245 | copy(mergedHandlers, group.Handlers)
246 | copy(mergedHandlers[len(group.Handlers):], handlers)
247 | return mergedHandlers
248 | }joinPaths 用标准库的 path.Join 拼起来,所以重复斜杠和 .. 会被化简;只有一个例外:相对路径以 / 结尾时,结果会补回结尾斜杠。GET("") 注册的是组自己的路径,GET("/") 注册的是带结尾斜杠的路径,两者是不同的路由。
Group("/api").GET("/users") → 路由 "/api/users",BasePath()="/api"
Group("/api/").GET("users") → 路由 "/api/users",BasePath()="/api/"
Group("/api").GET("users") → 路由 "/api/users",BasePath()="/api"
Group("/api").GET("/users/") → 路由 "/api/users/",BasePath()="/api"
Group("/api").GET("") → 路由 "/api",BasePath()="/api"
Group("/api").GET("/") → 路由 "/api/",BasePath()="/api"
Group("api").GET("users") → 路由 "/api/users",BasePath()="/api"
Group("/").GET("/users") → 路由 "/users",BasePath()="/"
Group("").GET("/users") → 路由 "/users",BasePath()="/"
Group("/api//v1").GET("//users") → 路由 "/api/v1/users",BasePath()="/api/v1"
Group("/api/v1/..").GET("/users") → 路由 "/api/users",BasePath()="/api"开头斜杠、结尾斜杠、空串、//、.. 的各种组合都落到了一个规整的绝对路径上。注意 Group("/api/") 的 BasePath() 保留了结尾斜杠("/api/"),拼路由时才被 path.Join 化简。
GET /ping → global [h] /global
GET /api/a → global api [h] /api /global
GET /api/v1/b → global api v1 [h] /v1 /api /global
GET /api/v1/admin/c → global api v1 admin c-only [h] /c-only /admin /v1 /api /global
树里:"b" [static prio=1 indices=""] handlers=4 full="/api/v1/b"
树里:"admin/c" [static prio=1 indices=""] handlers=6 full="/api/v1/admin/c"
树里:"a" [static prio=1 indices=""] handlers=3 full="/api/a"
树里:"ping" [static prio=1 indices=""] handlers=2 full="/ping"每一层 Group 把自己的 Handlers 复制一份再加上新的中间件,路由注册时再把路由自己的 handler 拼在最后。DumpTree 里能看到 /api/v1/admin/c 上有 6 个 handler。执行时 c.Next() 之前的代码从外向内,之后的代码从内向外。
GET /before → [h]
GET /after → late [h]
GET /g/own → late g-late [h]
GET /g/s/x → late [h]
GET /g2/early → late [h]
GET /g2/late → late g2-late [h]因为拼接发生在注册时(combineHandlers 复制出新切片),之后再调用 Use 影响不到已经注册的路由,也影响不到在 Use 之前创建的子组。典型的坑:先写路由、最后才补一个 r.Use(auth),前面的路由全都没有鉴权。
gin.go: 328 | engine.rebuild404Handlers()
gin.go: 334 | engine.rebuild405Handlers()
gin.go: 342 | engine.rebuild404Handlers()
gin.go: 343 | engine.rebuild405Handlers()
gin.go: 356 | func (engine *Engine) rebuild404Handlers() {
gin.go: 357 | engine.allNoRoute = engine.combineHandlers(engine.noRoute)
gin.go: 360 | func (engine *Engine) rebuild405Handlers() {
GET /g/ok → A G 状态 200
GET /g/nothing → A 状态 404
GET /nothing → A 状态 404全局 Engine.Use 会重建 404/405 的 handler 链,所以 404 页面也经过日志、恢复等全局中间件;Group.Use 只改组自己的 Handlers,所以 /g/nothing 这种“在组前缀下但没注册”的路径,404 时不会经过组里的中间件。