1.1 RouterGroup 结构
源码位置 :routergroup.go:53-62
go
type RouterGroup struct {
Handlers HandlersChain // 该组所有中间件
basePath string // 路径前缀
engine *Engine // 反向引用 Engine
root bool // 是否是根组(Engine 自身)
}
💡设计意图 :RouterGroup 是个非常轻 的对象(4 个字段)。
它存在的意义是:
- 提供"前缀 + 公共中间件"的抽象,让
r.Group("/api")这种写法很自然
- 让
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
}
什么分两层?
IRouter比IRoutes多了Group方法。
- 子组可以再
Group,但根Engine的Group方法返回*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 个动作:
combineHandlers(handlers)--- 合并父组的中间件 + 传入的中间件
calculateAbsolutePath(relativePath)--- 父组的 basePath + 相对路径
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
}
关键点:
finalSize < abortIndex校验:abortIndex = math.MaxInt8 >> 1 = 63,
所以单个请求的中间件总数不能超过 63(包含业务 handler)。
- 合并是新建切片 ,不是原地修改------这样子组修改中间件不会影响父组。
- 父组中间件在前,新中间件在后,执行顺序就是从父到子。
1.3.2 calculateAbsolutePath
csharp
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
joinPaths 在 path.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/... 的共同终点。三步:
- 算绝对路径
- 把当前组的中间件和传入 handlers 合并
- 交给 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()
}
特点:
- 静态路径不能带参数(panic)
- 同时注册 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) 内部:
handle("GET", "/users/:id", [getUser])
calculateAbsolutePath→/api/v1/users/:id
combineHandlers→[Logger, Recovery, Auth, getUser](4 个)
engine.addRoute("GET", "/api/v1/users/:id", [Logger, Recovery, Auth, getUser])
engine.trees.get("GET")找到 GET 树
root.addRoute("/api/v1/users/:id", handlers)插入 radix tree(详见第 3 章)
请求 /api/v1/users/42 进入时:
engine.handleHTTPRequest找到 GET 树
root.getValue("/api/v1/users/42", ...)返回handlers=[Logger, Recovery, Auth, getUser]和params=[{id, 42}]
c.handlers = value.handlers,c.Params = [{id, 42}]
c.Next()依次执行 Logger → Recovery → Auth → getUser

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