三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

路由注册:RouterGroup(routergroup.go)

路由注册:RouterGroup(routergroup.go)

1.1RouterGroup结构

源码位置:routergroup.go:53-62

type RouterGroup struct { Handlers HandlersChain // 该组所有中间件 basePath string // 路径前缀 engine *Engine // 反向引用 Engine root bool // 是否是根组(Engine 自身) }

💡设计意图:RouterGroup是个非常轻的对象(4 个字段)。
它存在的意义是:

  1. 提供"前缀 + 公共中间件"的抽象,让r.Group("/api")这种写法很自然
  1. Engine通过内嵌复用所有路由注册方法(r.GET(...)实际是r.RouterGroup.GET(...))

它不存路由树——树是Engine的字段。RouterGroup 只负责"算路径 + 拼中间件 + 转交 Engine"。

接口层次

// routergroup.go:26-30 type IRouter interface { IRoutes Group(string, ...HandlerFunc) *RouterGroup } // routergroup.go:33-51 type IRoutes interface { Use(...HandlerFunc) IRoutes Handle(string, string, ...HandlerFunc) IRoutes Any(string, ...HandlerFunc) IRoutes GET(string, ...HandlerFunc) IRoutes POST(string, ...HandlerFunc) IRoutes // ... DELETE / PATCH / PUT / OPTIONS / HEAD / Match StaticFile(string, string) IRoutes StaticFileFS(string, string, http.FileSystem) IRoutes Static(string, string) IRoutes StaticFS(string, http.FileSystem) IRoutes }

什么分两层?

  • IRouterIRoutes多了Group方法。
  • 子组可以再Group,但EngineGroup方法返回*RouterGroup(不是IRoutes)。
  • 这种拆分让类型清晰:能 Group 的就是 IRouter,只能注册路由的就是 IRoutes。


1.2 中间件:Use

源码位置:routergroup.go:64-68

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() }

就这么简单——把中间件追加到group.Handlers切片末尾。

returnObj()routergroup.go:254-259:

func (group *RouterGroup) returnObj() IRoutes { if group.root { return group.engine // 根组返回 Engine(IRoutes 实现) } return group // 子组返回自己 }

💡设计意图:让链式 API 在不同上下文返回合适类型。
全局r.Use(...).GET(...)直接返回*Engine,方便继续操作;
子组g.Use(...).GET(...)返回*RouterGroup


1.3 创建子组:Group

源码位置:routergroup.go:70-78

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { return &RouterGroup{ Handlers: group.combineHandlers(handlers), basePath: group.calculateAbsolutePath(relativePath), engine: group.engine, } }

3 个动作:

  1. combineHandlers(handlers)— 合并父组的中间件 + 传入的中间件
  1. calculateAbsolutePath(relativePath)— 父组的 basePath + 相对路径
  1. engine: group.engine— 共享同一个 Engine(也就是同一棵路由树)

注意:子组没有root字段——它的 zero value 是false,正是我们想要的。

1.3.1combineHandlers—— 中间件合并的核心

源码位置:routergroup.go:241-248

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) assert1(finalSize < int(abortIndex), "too many handlers") mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers }

关键点:

  1. finalSize < abortIndex校验:abortIndex = math.MaxInt8 >> 1 = 63,
    所以单个请求的中间件总数不能超过 63(包含业务 handler)。
  1. 合并是新建切片,不是原地修改——这样子组修改中间件不会影响父组
  1. 父组中间件在前,新中间件在后,执行顺序就是从父到子。

1.3.2calculateAbsolutePath

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { return joinPaths(group.basePath, relativePath) }

joinPathspath.go实现(下一章细讲),做路径拼接。

1.3.3 嵌套示例

r := gin.Default() // basePath="/", Handlers=[] api := r.Group("/api", AuthMiddleware) // basePath="/api", Handlers=[AuthMiddleware] v1 := api.Group("/v1", RateLimitMiddleware) // basePath="/api/v1", Handlers=[AuthMiddleware, RateLimitMiddleware] v1.GET("/users", h) // 注册到 /api/v1/users,handlers=[AuthMiddleware, RateLimitMiddleware, h]

1.4 注册路由:handle

源码位置:routergroup.go:86-91

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() }

这是所有 GET/POST/PUT/... 的共同终点。三步:

  1. 算绝对路径
  1. 把当前组的中间件和传入 handlers 合并
  1. 交给 Engine 的addRoute(第 2 章已分析)

1.4.1 快捷方法

源码位置:routergroup.go:103-153

func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { if matched := regEnLetter.MatchString(httpMethod); !matched { panic("http method " + httpMethod + " is not valid") } return group.handle(httpMethod, relativePath, handlers) } func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodPost, relativePath, handlers) } func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodGet, relativePath, handlers) } // ... DELETE / PATCH / PUT / OPTIONS / HEAD 同构 func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes { for _, method := range anyMethods { group.handle(method, relativePath, handlers) } return group.returnObj() } func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes { for _, method := range methods { group.handle(method, relativePath, handlers) } return group.returnObj() }

每个 HTTP 方法的注册函数都是一行包装,核心都在handle

anyMethods(routergroup.go:18-24):

anyMethods = []string{ http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect, http.MethodTrace, }

📌注意:Any不包含扩展方法(如 WebDAV 的 PROPFIND)。
如需自定义方法,用Handle("PROPFIND", ...)


1.5 静态文件

1.5.1StaticFile—— 单个文件

源码位置:routergroup.go:164-188

func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { return group.staticFileHandler(relativePath, func(c *Context) { c.File(filepath) }) } func (group *RouterGroup) staticFileHandler(relativePath string, handler HandlerFunc) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static file") } group.GET(relativePath, handler) group.HEAD(relativePath, handler) return group.returnObj() }

特点:

  1. 静态路径不能带参数(panic)
  1. 同时注册 GET 和 HEAD(后者用于客户端只取 headers)

1.5.2Static/StaticFS—— 整个目录

源码位置:routergroup.go:190-239

func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static folder") } handler := group.createStaticHandler(relativePath, fs) urlPattern := path.Join(relativePath, "/*filepath") group.GET(urlPattern, handler) group.HEAD(urlPattern, handler) return group.returnObj() } func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc { absolutePath := group.calculateAbsolutePath(relativePath) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) return func(c *Context) { if _, noListing := fs.(*OnlyFilesFS); noListing { c.Writer.WriteHeader(http.StatusNotFound) } file := c.Param("filepath") f, err := fs.Open(file) if err != nil { c.Writer.WriteHeader(http.StatusNotFound) c.handlers = group.engine.noRoute c.index = -1 // ★ 重置中间件链,走 noRoute return } f.Close() fileServer.ServeHTTP(c.Writer, c.Request) } }

巧妙之处:

  • 路由模式/assets/*filepath(用通配符)
  • 复用标准库http.FileServer做实际服务
  • 文件不存在时,手动改写c.handlersc.index,让流程走noRoute兜底

💡c.index = -1这一行很关键,详见第 6 章对Next的分析。


1.6IRoutervsIRoutes:为什么这么设计?

// IRouter 内嵌 IRoutes,并加了 Group type IRouter interface { IRoutes Group(string, ...HandlerFunc) *RouterGroup }

设计意图:让用户写的辅助函数能"接收任意路由组"。

// 注册一组路由:既可以是 Engine,也可以是 RouterGroup func RegisterUserRoutes(r gin.IRoutes) { r.GET("/users", listUsers) r.POST("/users", createUser) } // 用法 1:全局 RegisterUserRoutes(r) // 用法 2:子组 v1 := r.Group("/v1") RegisterUserRoutes(v1)

如果只用*Engine*RouterGroup,这种通用性就做不到。


1.7 看不见的细节

1.7.1 注册顺序很重要

r.GET("/users/me", meH) // 先注册静态 r.GET("/users/:id", idH) // 后注册参数

由于 radix tree 的"静态优先"机制,顺序不影响最终匹配结果。
但如果是同一段内冲突(:idvs:name),启动时会 panic。

1.7.2 注册时的 debug 输出

addRoute会调debugPrintRoute,在 debug 模式下打印:

[GIN-debug] GET /users/:id --> main.idH (3 handlers)

第三个数字(3 handlers)表示合并后的 handlers 长度。如果你看到(1 handlers),
意味着没有任何中间件——通常意味着gin.New()没挂 Logger/Recovery。

1.7.3combineHandlers的容量校验

assert1(finalSize < int(abortIndex), "too many handlers")

abortIndex = math.MaxInt8 >> 1 = 63,所以单个请求的中间件 + handler 总数 ≤ 62
对绝大多数项目远超够用,但写中间件堆叠特别多的代码要注意。


1.8 完整流程示例

r := gin.Default() // basePath="/", Handlers=[Logger, Recovery] api := r.Group("/api", Auth) // basePath="/api", Handlers=[Logger, Recovery, Auth] v1 := api.Group("/v1") // basePath="/api/v1", Handlers=[Logger, Recovery, Auth] v1.GET("/users/:id", getUser) // 注册路由

最后一步v1.GET("/users/:id", getUser)内部:

  1. handle("GET", "/users/:id", [getUser])
  1. calculateAbsolutePath/api/v1/users/:id
  1. combineHandlers[Logger, Recovery, Auth, getUser](4 个)
  1. engine.addRoute("GET", "/api/v1/users/:id", [Logger, Recovery, Auth, getUser])
  1. engine.trees.get("GET")找到 GET 树
  1. root.addRoute("/api/v1/users/:id", handlers)插入 radix tree(详见第 3 章)

请求/api/v1/users/42进入时:

  1. engine.handleHTTPRequest找到 GET 树
  1. root.getValue("/api/v1/users/42", ...)返回handlers=[Logger, Recovery, Auth, getUser]params=[{id, 42}]
  1. c.handlers = value.handlers,c.Params = [{id, 42}]
  1. c.Next()依次执行 Logger → Recovery → Auth → getUser


1.9 小结

  • RouterGroup是个轻对象,只负责算路径 + 拼中间件 + 转交 Engine
  • Engine内嵌RouterGroup,所以r.GET/r.Use都能用
  • Group创建子组时,会复制父组的中间件(不污染父组)
  • ✅ 所有 HTTP 方法的注册函数最终走handleengine.addRoute
  • ✅ 静态文件复用标准库http.FileServer,只是套了通配符路由
← 返回列表