This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hzh/GIN/1-gin-architecture.md
T

289 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 基础用法入门