Files
cs-note/hzh/GIN/1-gin-architecture.md
T
2026-05-24 11:42:38 +08:00

10 KiB
Raw Blame History

tags, create time
tags create time
后端
Go
Gin
架构
原理
2026-04-27 10:01

Gin 整体架构

概述

Gin 的本质是一个 http.Handler——它没有脱离 Go 标准库 net/http,而是在其之上做了三层增强:

  1. 高性能路由匹配(Radix Tree 基数树)
  2. 中间件链(可组合的横切逻辑)
  3. 上下文对象(*gin.Context 统一管理请求/响应/参数/错误)

理解 Gin 架构的起点,是认清它和 net/http 的边界——Gin 不替代标准库,而是包装和增强它。

思考题:gin.Default() 返回的是一个 *Engine,而 *Engine 实现了 http.Handler 接口。这意味着什么?能不能直接把 Gin Engine 传给标准库的 http.ListenAndServe?(详见 1-gin-architecture/engine-handler)

正文

1. Gin 的核心组件

Gin 有三大核心对象,它们的关系可以一句话概括:

Engine 是引擎,RouterGroup 是路由组织单元,Context 是请求的生命周期容器。

graph LR
    A["*gin.Engine"] -->|"管理"| B["*routerGroup[]"]
    A -->|"提供"| C["*gin.Context"]
    A -->|"包含"| D["radix tree"]
    B -->|"挂载路由"| D
    B -->|"生成"| C
    style A fill:#e3f2fd,stroke:#1565c0
    style B fill:#fff3e0,stroke:#e65100
    style C fill:#e8f5e9,stroke:#2e7d32

Engine — 全局引擎

Engine 是 Gin 的心脏,一个 Gin 应用有且只有一个 Engine:

type Engine struct {
    RouterGroup           // 嵌入,Engine 本身就是最大的 RouterGroup
    redirectTrailingSlash bool
    redirectFixedPath     bool
    handleMethodNotAllowed bool
    trees                 methodTrees  // 按 HTTP 方法组织的路由树
    // ...
}

关键细节:

  • Engine 嵌入了 RouterGroup,所以 r := gin.New() 创建的 Engine 本身就是一个 RouterGroup,可以直接调 .GET()、.Use()
  • trees 是一个 methodTrees(本质是 [*tree]),每种 HTTP 方法一棵独立的 Radix Tree——这就是 Gin 高性能的核心原因之一
  • Engine 实现了 http.Handler 接口的 ServeHTTP(c http.ResponseWriter, r *http.Request) 方法

思考题:为什么 Gin 要为每个 HTTP 方法建一棵独立的树,而不是把所有方法塞进同一棵?

RouterGroup — 路由组织单元

RouterGroup 负责路由的层级组织:

type RouterGroup struct {
    RelativePath string                 // 相对路径前缀,如 "/api/users"
    handlers     HandlersChain          // 该分组下的中间件链
    engine       *Engine                // 指向全局引擎
    basePath     string                 // 绝对路径前缀
}
  • Group(path string) 创建子分组,自动继承父分组的中间件和前缀
  • 注册路由时,最终路径 = basePath + relativePath
  • Engine 本身就是根分组(basePath = "")

Context — 请求生命周期容器

*gin.Context 贯穿单个请求的整个生命周期:

type Context struct {
    writermem  ResponseWriter        // 响应缓冲区
    Request    *http.Request         // 原始请求
    fullPath   string                // 完整路由路径
    handlers   HandlersChain         // 待执行的 handler 链
    index      int8                  // 当前执行到第几个 handler
    engine     *Engine               // 回指引擎
    params     *Params               // 路径参数
    errors     ErrorList             // 错误链
    keys       map[string]interface{} // 请求级存储
}

重点理解: 每个请求都会创建一个独立的 *gin.Context,对象会复用(sync.Pool),但 keys map 是每个请求隔离的。

提问: gin.Context 被 sync.Pool 复用,那如果在 handler 中把 c 保存到全局变量里,下次请求读到的是什么?(详见 1-gin-architecture/context-pool)

2. 请求生命周期(从 HTTP 到 Handler)

这是 Gin 最核心的流程——从收到一个原始 HTTP 请求到最终返回响应,中间经历了什么:

sequenceDiagram
    participant C as Client
    participant L as ListenAndServe
    participant E as Engine.ServeHTTP
    participant R as Route Match
    participant M as Middleware Chain
    participant H as User Handler
    participant W as Response

    C->>L: HTTP Request
    L->>E: ServeHTTP(rw, req)
    E->>E: 按方法查找路由树 (trees[method])
    E->>R: 匹配路径 → handler chain
    alt 匹配成功
        E->>M: 创建 gin.Context,执行中间件链
        M->>H: c.Next() 进入用户 handler
        H->>W: c.JSON() / c.String() 写入响应
        H->>M: 返回,中间件继续执行
        M->>E: 所有 handler 执行完毕
    else 匹配失败:路径存在但方法不对 (405)
        E->>W: c.JSON(405, "Method Not Allowed")
    else 匹配失败:路径不存在 (404)
        E->>W: 404 Not Found
    end
    E->>L: rw 写入 http.ResponseWriter
    L->>C: HTTP Response

逐步拆解:

第一步:路由匹配

// gin/engine.go — 简化版
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 1. 按方法找到对应的 Radix Tree
    c := engine.getContext()       // 从 sync.Pool 取 Context
    c.Reset()                       // 清空旧数据
    c.Request = req
    c.writermem.reset(w)

    // 2. 匹配路径
    handlers, c.Params, search := engine.tree.match(req.URL.Path, req.Method)

    // 3. 分配 handler 链
    c.handlers = handlers
    c.index = -1                    // 从第一个 handler 开始

    // 4. 分发执行
    c.Next()                        // ← 进入中间件链
    engine.freeContext(c)           // 归还到 sync.Pool
}

第二步:中间件链执行

// gin/context.go — Next() 的核心逻辑
func (c *Context) Next() {
    c.index++                       // 推进索引
    // 执行当前 index 对应的 handler
    for ; c.index < int8(len(c.handlers)); c.index++ {
        c.handlers[c.index](c)      // 执行中间件/handler
    }
}

这就是经典的 倒序执行模式:

flowchart LR
    A["中间件A (index=0)"] -->|"c.Next()"| B["中间件B (index=1)"]
    B -->|"c.Next()"| C["Handler (index=2)"]
    C -->|"执行完毕"| B2["中间件B 继续"]
    B2 -->|"继续"| A2["中间件A 继续"]

    style C fill:#e8f5e9,stroke:#2e7d32
    style B fill:#fff3e0,stroke:#e65100
    style B2 fill:#fff3e0,stroke:#e65100
    style A2 fill:#e3f2fd,stroke:#1565c0

思考题:如果中间件 A 中 c.Next() 之前打印 "before",之后打印 "after",中间件 B 也这样做,那一个请求经过 A→B→Handler 后,输出的顺序是什么?

3. Gin 与标准库 net/http 的关系

很多开发者误以为 Gin 替代了 net/http,实际上它只是在标准库之上做了一个有选择的增强:

graph TD
    A["net/http"] -->|"提供"| B["Server, Listener, ResponseWriter"]
    A -->|"提供"| C["Request, Response, Header"]
    A -->|"提供"| D["ServeMux 路由"]

    E["Gin"] -->|"替代"| F["ServeMux → Radix Tree 路由"]
    E -->|"包装"| G["ResponseWriter → buffered writer"]
    E -->|"增强"| H["Request → gin.Context 包装"]
    E -->|"扩展"| I["中间件链 + Context + 绑定 + 渲染"]

    B --> E
    C --> E
    style E fill:#fff9c4,stroke:#f57f17,stroke-width:3px

Gin 替代了什么:

  • http.ServeMux → Radix Tree 路由(支持通配符、参数、更优性能)
  • http.HandlerFunc → gin.HandlerFunc + 中间件链
  • 手动 json.NewEncoder → c.JSON()

Gin 保留了什么:

  • http.Request(c.Request 原封不动)
  • http.ResponseWriter(用 responseWriter 包装,加了缓冲)
  • http.ListenAndServe(r.Run() 最终调的也是这个)
  • 所有 net/http 的类型和接口

核心结论: Gin 不是另一个 HTTP 框架,它是 net/http 的增强层。所有标准库的知识完全适用于 Gin,反之则不然——你会 Gin 不代表你懂 net/http。

4. Gin 的初始化链

gin.Default() 背后做了三件事:

// gin.Default() 等价于:
func Default() *Engine {
    debugPrintWARNINGDefault()       // 调试模式下打印提示
    engine := New()                  // 创建不带中间件的 Engine
    engine.Use(Logger(), Recovery()) // 挂载日志和恢复中间件
    return engine
}
初始化方式 包含中间件 适用场景
gin.New() 无 完全自定义日志、测试、最小化开销
gin.Default() Logger + Recovery 快速开发、默认推荐
func main() {
    r := gin.Default()  // = gin.New() + Logger() + Recovery()

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })

    // r.Run() 内部实现:
    // http.ListenAndServe(":8080", r)
    // 注意:Engine 实现了 http.Handler,可以直接传给标准库
    r.Run()
}

提问: gin.New() 不包含 Logger 中间件,那如果直接用 gin.New() 启动服务,请求进来时你会看到什么日志?如果想自己加日志但用自定义格式,应该怎么做?

5. 对象复用与性能设计

Gin 在几个关键地方做了对象复用,减少 GC 压力:

graph LR
    A["sync.Pool"] -->|"复用"| B["*gin.Context"]
    A -->|"复用"| C["*responseWriter"]
    A -->|"复用"| D["*params"]

    E["*Engine"] -->|"全局单例,不复用"| F["路由树"]

    style A fill:#e8f5e9,stroke:#2e7d32
    style F fill:#fff3e0,stroke:#e65100
  • *gin.Context:每次请求从 pool 取出,用完归还(engine.freeContext(c))
  • *responseWriter:缓冲写入器,复用减少分配
  • 路由树 *tree:全局唯一,永久驻留内存
  • *Engine:全局唯一,永久驻留

思考题: Context 被池化复用,那在 handler 中启动 goroutine 并在 goroutine 里使用 c,会遇到什么问题?这和我们后面要讲的 c.Copy() 有什么关系?

关联笔记

  • [[GIN/routing]] — 路由匹配算法深入(Radix Tree)
  • [[GIN/middleware]] — 中间件机制与执行顺序
  • [[GIN/context-lifecycle]] — Context 的底层设计与陷阱
  • [[1-gin-architecture/engine-handler]] — Gin 与 http.Handler 的关系
  • [[Go 后端基础]] — Gin 基础用法入门