289 lines
10 KiB
Markdown
289 lines
10 KiB
Markdown
---
|
||
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 基础用法入门
|