路由注册:RouterGroup(routergroup.go)

1.1 RouterGroup 结构

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

go 复制代码
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"。

接口层次

scss 复制代码
// 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

scss 复制代码
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

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

returnObj()routergroup.go:254-259:

csharp 复制代码
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

vbnet 复制代码
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.1 combineHandlers ------ 中间件合并的核心

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

go 复制代码
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.2 calculateAbsolutePath

csharp 复制代码
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
    return joinPaths(group.basePath, relativePath)
}

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

1.3.3 嵌套示例

ini 复制代码
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

csharp 复制代码
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

go 复制代码
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):

ini 复制代码
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.1 StaticFile ------ 单个文件

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

scss 复制代码
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.2 Static / StaticFS ------ 整个目录

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

scss 复制代码
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.handlers c.index,让流程走 noRoute 兜底

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


1.6 IRouter vs IRoutes:为什么这么设计?

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

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

scss 复制代码
// 注册一组路由:既可以是 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 注册顺序很重要

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

由于 radix tree 的"静态优先"机制,顺序不影响最终匹配结果。

但如果是同一段内冲突 (:id vs :name),启动时会 panic。

1.7.2 注册时的 debug 输出

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

bash 复制代码
[GIN-debug] GET    /users/:id     --> main.idH (3 handlers)

第三个数字 (3 handlers) 表示合并后的 handlers 长度。如果你看到 (1 handlers),

意味着没有任何中间件------通常意味着 gin.New() 没挂 Logger/Recovery。

1.7.3 combineHandlers 的容量校验

c 复制代码
assert1(finalSize < int(abortIndex), "too many handlers")

abortIndex = math.MaxInt8 >> 1 = 63,所以单个请求的中间件 + handler 总数 ≤ 62

对绝大多数项目远超够用,但写中间件堆叠特别多的代码要注意。


1.8 完整流程示例

css 复制代码
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,只是套了通配符路由
相关推荐
长栎1 小时前
你用了五年的消息队列,不知道它背后站着中介者模式
后端
用户608186527901 小时前
Avalonia UI 控件样式定义的三种方式详解
后端
feng尘1 小时前
volatile 可见性与内存屏障知识点
后端
SamDeepThinking1 小时前
第3篇:企业级CAS单点登录实战-技术架构设计方案
后端·程序员·架构
JoyT1 小时前
Agent 开源项目全景解析(上):LangGraph、Spring AI 与 Agent Runtime
后端
云技纵横1 小时前
线上接口突然超时,怎么判断卡在 Nginx、线程池、连接池还是 SQL?
后端·sql·mysql
Java内核笔记1 小时前
容错能力进入 spring-core:Spring Boot 4 原生重试机制全解析
java·后端
风卿1 小时前
知识库双路召回:BM25 关键词与语义向量 RRF 融合,附指标实测
后端
未秃头的程序猿1 小时前
虚拟线程上线一周后翻车了——pinning问题排查实录
java·后端·架构