Files
cs-note/hzh/GIN/1-gin-architecture.md
T

289 lines
10 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
tags: [后端, Go, Gin, 架构, 原理]
create time: 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 是请求的生命周期容器。**
```mermaid
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:
```go
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` 负责路由的层级组织:
```go
type RouterGroup struct {
RelativePath string // 相对路径前缀,如 "/api/users"
handlers HandlersChain // 该分组下的中间件链
engine *Engine // 指向全局引擎
basePath string // 绝对路径前缀
}
```
- `Group(path string)` 创建子分组,自动继承父分组的中间件和前缀
- 注册路由时,最终路径 = `basePath` + `relativePath`
- Engine 本身就是根分组(`basePath = ""`)
#### Context — 请求生命周期容器
`*gin.Context` 贯穿单个请求的整个生命周期:
```go
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 请求到最终返回响应,中间经历了什么:
```mermaid
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
```
**逐步拆解:**
**第一步:路由匹配**
```go
// 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
}
```
**第二步:中间件链执行**
```go
// 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
}
}
```
这就是经典的 **倒序执行模式**:
```mermaid
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`,实际上它只是在标准库之上做了一个**有选择的增强**:
```mermaid
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()` 背后做了三件事:
```go
// gin.Default() 等价于:
func Default() *Engine {
debugPrintWARNINGDefault() // 调试模式下打印提示
engine := New() // 创建不带中间件的 Engine
engine.Use(Logger(), Recovery()) // 挂载日志和恢复中间件
return engine
}
```
| 初始化方式 | 包含中间件 | 适用场景 |
|-----------|-----------|----------|
| `gin.New()` | 无 | 完全自定义日志、测试、最小化开销 |
| `gin.Default()` | Logger + Recovery | 快速开发、默认推荐 |
```go
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 压力:
```mermaid
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 基础用法入门