bigywater.tech
Gin · 9

Group 的 basePath 合并与中间件继承

Group 只是路径前缀加一份中间件的拷贝?这篇逐个验证:路径怎么拼、嵌套组的中间件怎么叠加、什么时候“固化”,以及一个容易忽略的结论——在组里 Use 的中间件管不到这个前缀下的 404。

6 个
/api/v1/admin/c 这条路由最终的 handler 数
0 次
子组在 g.Use 之前创建时,它的路由里 g-late 执行的次数
A,没有 G
请求 /g/nothing(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("/") 注册的是带结尾斜杠的路径,两者是不同的路由。

11 种拼法

实测 · 本机执行输出
    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() 之前的代码从外向内,之后的代码从内向外。

Use 的时机

实测 · 本机执行输出
    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),前面的路由全都没有鉴权。

404 与组中间件

实测 · 本机执行输出
  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 时不会经过组里的中间件。

分步演示

Group 与 basePath
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。DumpTree 是源码副本里加的只读打印函数(用来数每条路由的 handler 个数);中间件和 handler 都用 fmt.Print 记录执行顺序;请求用 httptest 直接调用 ServeHTTP;每种情形各一次
    • 没有验证:Group.Use 对 NoRoute / NoMethod 自定义 handler 的影响(只验证了默认 404);大量嵌套(超过 63 个 handler 的上限 abortIndex)时的 panic 文案(源码里有断言,没有触发);并发地创建 Group(Gin 要求注册路由时单线程,没有测);Group 与 Static 系列函数组合时的路径拼接
    京ICP备2026067502号-1京公网安备11010502063487号