From 6a12884ea7453a6512e898826042eadad7ba911a Mon Sep 17 00:00:00 2001 From: wonder Date: Tue, 28 Apr 2026 19:33:43 +0800 Subject: [PATCH] vault backup: 2026-04-28 19:33:43 --- hzh/GIN/1-gin-architecture.md | 288 +++++++++ hzh/GIN/1-gin-architecture/context-pool.md | 168 ++++++ hzh/GIN/1-gin-architecture/engine-handler.md | 168 ++++++ hzh/GIN/10-template-rendering.md | 312 ++++++++++ hzh/GIN/11-static-files.md | 194 +++++++ hzh/GIN/12-server-config.md | 193 +++++++ hzh/GIN/13-graceful-shutdown.md | 208 +++++++ hzh/GIN/14-logging.md | 242 ++++++++ hzh/GIN/15-advanced-running.md | 184 ++++++ hzh/GIN/2-routing.md | 293 ++++++++++ .../2-routing-complexity-comparison.md | 80 +++ hzh/GIN/3-middleware.md | 397 +++++++++++++ .../3-middleware/cors-registration-scope.md | 93 +++ .../cors-registration-scope/cors-preflight.md | 184 ++++++ hzh/GIN/3-middleware/gin-vs-std.md | 89 +++ hzh/GIN/3-middleware/jwt-auth-qa.md | 105 ++++ hzh/GIN/3-middleware/middleware-abort.md | 130 +++++ hzh/GIN/4-context-lifecycle.md | 228 ++++++++ .../context-pool-safety.md | 109 ++++ hzh/GIN/5-binding-validation.md | 419 ++++++++++++++ .../5-binding-validation/unknown-fields.md | 160 +++++ hzh/GIN/6-error-handling.md | 545 ++++++++++++++++++ hzh/GIN/6-error-handling/q-biz-code-header.md | 77 +++ .../6-error-handling/q-c-error-nil-panic.md | 97 ++++ hzh/GIN/7-binding-advanced.md | 288 +++++++++ hzh/GIN/7-binding-advanced/skip-binding.md | 131 +++++ hzh/GIN/8-file-upload.md | 530 +++++++++++++++++ hzh/GIN/9-response-rendering.md | 408 +++++++++++++ hzh/GIN/README.md | 98 ++++ 29 files changed, 6418 insertions(+) create mode 100644 hzh/GIN/1-gin-architecture.md create mode 100644 hzh/GIN/1-gin-architecture/context-pool.md create mode 100644 hzh/GIN/1-gin-architecture/engine-handler.md create mode 100644 hzh/GIN/10-template-rendering.md create mode 100644 hzh/GIN/11-static-files.md create mode 100644 hzh/GIN/12-server-config.md create mode 100644 hzh/GIN/13-graceful-shutdown.md create mode 100644 hzh/GIN/14-logging.md create mode 100644 hzh/GIN/15-advanced-running.md create mode 100644 hzh/GIN/2-routing.md create mode 100644 hzh/GIN/2-routing/2-routing-complexity-comparison.md create mode 100644 hzh/GIN/3-middleware.md create mode 100644 hzh/GIN/3-middleware/cors-registration-scope.md create mode 100644 hzh/GIN/3-middleware/cors-registration-scope/cors-preflight.md create mode 100644 hzh/GIN/3-middleware/gin-vs-std.md create mode 100644 hzh/GIN/3-middleware/jwt-auth-qa.md create mode 100644 hzh/GIN/3-middleware/middleware-abort.md create mode 100644 hzh/GIN/4-context-lifecycle.md create mode 100644 hzh/GIN/4-context-lifecycle/context-pool-safety.md create mode 100644 hzh/GIN/5-binding-validation.md create mode 100644 hzh/GIN/5-binding-validation/unknown-fields.md create mode 100644 hzh/GIN/6-error-handling.md create mode 100644 hzh/GIN/6-error-handling/q-biz-code-header.md create mode 100644 hzh/GIN/6-error-handling/q-c-error-nil-panic.md create mode 100644 hzh/GIN/7-binding-advanced.md create mode 100644 hzh/GIN/7-binding-advanced/skip-binding.md create mode 100644 hzh/GIN/8-file-upload.md create mode 100644 hzh/GIN/9-response-rendering.md create mode 100644 hzh/GIN/README.md diff --git a/hzh/GIN/1-gin-architecture.md b/hzh/GIN/1-gin-architecture.md new file mode 100644 index 0000000..70cc765 --- /dev/null +++ b/hzh/GIN/1-gin-architecture.md @@ -0,0 +1,288 @@ +--- +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 基础用法入门 diff --git a/hzh/GIN/1-gin-architecture/context-pool.md b/hzh/GIN/1-gin-architecture/context-pool.md new file mode 100644 index 0000000..0226e1c --- /dev/null +++ b/hzh/GIN/1-gin-architecture/context-pool.md @@ -0,0 +1,168 @@ +--- +tags: + - 后端 + - Go + - Gin + - 并发 +create time: 2026-04-27 10:05 +--- + +# Context 池化与常见陷阱 + +## 概述 + +`gin.Context` 通过 `sync.Pool` 复用以减少 GC 压力,这带来了性能红利,也埋下了并发陷阱。本文深入剖析池化机制、保存 Context 引用会引发的数据错乱问题,以及如何正确使用。 + +## 正文 + +### 1. Context 的复用流程 + +Gin 在每次请求的完整生命周期中复用同一个 `*gin.Context` 对象: + +```go +// gin/engine.go — ServeHTTP 简化版 +func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c := engine.getContext() // 从 sync.Pool 取 Context(可能是旧的) + c.Reset() // 清空所有字段,洗白板 + c.Request = r + c.writermem.reset(w) + + handlers, params, _ := engine.tree.match(r.URL.Path, r.Method) + c.handlers = handlers + c.params = params + c.index = -1 + + c.Next() // 执行中间件链 + 用户 handler + + engine.freeContext(c) // 归还到 sync.Pool,等待下次复用 +} +``` + +关键链路:**从 pool 取 → Reset 清空 → 填充新数据 → 执行完毕 → 归还 pool**。 + +这意味着 `sync.Pool` 里存的是**被洗过白板的对象**,不是干净的新对象。 + +### 2. 陷阱:保存 c 到全局变量 + +如果在 handler 中把 `c` 保存到全局变量,**下次请求读到的是新请求的上下文**——因为对象被复用,字段被覆盖。 + +#### 错误示范 + +```go +var globalC *gin.Context + +func SaveHandler(c *gin.Context) { + globalC = c // 保存的是对象引用,不是副本 + c.String(200, "saved") +} + +func GetHandler(c *gin.Context) { + // 期望读到上次请求的数据,实际读到的是某个并发请求的数据! + req := globalC.Request + fmt.Println(req.URL.Path) // ← 可能是任意请求的路径 +} +``` + +#### 字段被覆盖对照表 + +| 字段 | 请求 A 写入 | 请求 B 到来后(`c.Reset()`) | +|------|-----------|--------------------------| +| `globalC.Request` | A 的 `*http.Request` | **变成 B 的 Request** | +| `globalC.keys` | `map[string]any{"user": "alice"}` | **被置为 nil,请求 B 重新 Set 后恢复** | +| `globalC.Params` | A 的路径参数 | **变成 B 的路径参数** | +| `globalC.writermem` | A 的响应缓冲 | **被重置,A 的已写入响应数据丢失** | + +#### 并发场景下的灾难 + +当多个并发请求到来时,问题会进一步恶化: + +``` +时间线: +T1: 请求 A 进入 → globalC = ctx(指向 pool 中的对象 X) +T2: 请求 B 进入 → ctx 归还 pool → 新请求 C 从 pool 取出同一个对象 X +T3: 请求 C 的 c.Reset() → globalC 指向的对象被清空 → 请求 A 的数据丢失! +T4: GetHandler 读取 globalC → 读到的是请求 C 的数据,不是 A 的 +``` + +**这不是简单的数据竞争,而是数据错乱**——你读到的既不是上次请求的数据,也不是当前请求的数据,而是**某个并发请求正在使用的数据**。 + +### 3. 正确做法 + +#### 方案一:拷贝数据而非保存引用 + +```go +var uid string + +func SaveHandler(c *gin.Context) { + uid = c.GetString("user_id") // 拷贝值,不是保存 c +} + +func GetHandler(c *gin.Context) { + _ = uid // 安全:拷贝的是基本类型值 +} +``` + +#### 方案二:c.Set + c.Copy(推荐用于 goroutine) + +如果需要在异步 goroutine 中使用请求数据: + +```go +func MyHandler(c *gin.Context) { + c.Set("user_id", "123") + + // ✅ 正确:c.Copy() 创建独立副本 + go func() { + c2 := c.Copy() // 深拷贝 Context,keys 被浅拷贝到新 map + time.Sleep(1 * time.Second) + // c2 在这里是安全的,不受 pool 复用影响 + fmt.Println(c2.GetString("user_id")) // "123" + }() +} +``` + +`c.Copy()` 的核心实现: + +```go +func (c *Context) Copy() *Context { + copy := &Context{ + writermem: c.writermem.Clone(), // 克隆响应缓冲 + Params: c.Params, // Params 是 []Param,线程安全 + engine: c.engine, + } + // 浅拷贝 keys map + if c.keys != nil { + copy.keys = copyMap(c.keys) // 创建新 map,拷贝所有键值对 + } + copy.Request = c.Request // *http.Request 本身是只读的,共享安全 + return copy +} +``` + +> **重点理解**:`c.Copy()` 做浅拷贝——`keys` map 本身是新的(键值对也是副本),但 `Request` 共享同一个 `*http.Request` 指针(标准库保证了 handler 执行期间 Request 不会被修改)。 + +### 4. 设计原则总结 + +``` +Context 池化 = 性能红利 + 并发陷阱 + +✅ 安全: + - 在 handler 同步代码中使用 c + - 传给 goroutine 前先用 c.Copy() + - 只拷贝需要的数据值(c.GetString 等) + +❌ 危险: + - 保存 c 的引用到全局/包级变量 + - 在 handler 中启动 goroutine 直接使用 c + - 跨请求传递 c 的引用 +``` + +### 5. 思考题 + +1. `c.Copy()` 是深拷贝还是浅拷贝?如果 `c.Set("user", userObj)` 存了一个指针类型,拷贝后两个 Context 里的 `userObj` 指向同一个对象吗?修改其中一个会互相影响吗? +2. `sync.Pool` 的 Get/Put 是线程安全的,那从 pool 取出的 Context 在并发请求场景下,有没有可能两个请求同时拿到同一个对象?为什么不会? + +## 关联笔记 + +- [[1-gin-architecture]] — Gin 整体架构 +- [[middleware]] — 中间件机制 +- [[engine-handler]] — Gin 与 http.Handler 的关系 diff --git a/hzh/GIN/1-gin-architecture/engine-handler.md b/hzh/GIN/1-gin-architecture/engine-handler.md new file mode 100644 index 0000000..0b82d3d --- /dev/null +++ b/hzh/GIN/1-gin-architecture/engine-handler.md @@ -0,0 +1,168 @@ +--- +tags: + - 后端 + - Go + - Gin + - 标准库 +create time: 2026-04-27 10:05 +--- + +# Gin 与 http.Handler 的关系 + +## 概述 + +`*gin.Engine` 实现了 `http.Handler` 接口,这使得 Gin 能够无缝接入 Go 标准库 `net/http` 生态。本文深入剖析这一设计背后的原理、意义和实际用法。 + +## 正文 + +### 1. 接口匹配:为什么能传? + +一切源于一个简单的类型匹配: + +```go +// 标准库定义 +type Handler interface { + ServeHTTP(ResponseWriter, *Request) +} + +// Gin 实现了它 +func (engine *Engine) ServeHTTP(w ResponseWriter, r *Request) +``` + +`*gin.Engine` 的 `ServeHTTP` 方法签名与 `http.Handler` 完全吻合,所以赋值关系天然成立: + +```go +var _ http.Handler = (*gin.Engine)(nil) // 编译期确认实现 +``` + +### 2. 三种启动方式,底层都一样 + +| 方式 | 代码 | 本质 | +|------|------|------| +| `r.Run()` | 框架封装 | `http.ListenAndServe(":8080", r)` | +| `http.ListenAndServe` | 标准库 | 直接传入 `*gin.Engine` | +| `http.Server{Handler: r}` | 高级控制 | 可自定义 ReadTimeout、TLS 等 | + +**方式一:`r.Run()`(最常用)** + +```go +r := gin.Default() +r.Run(":8080") // 默认 :8080,可省略端口 +``` + +**方式二:标准库直接启动** + +```go +r := gin.Default() +http.ListenAndServe(":8080", r) // 完全等价 +``` + +**方式三:`http.Server`(生产推荐)** + +```go +r := gin.Default() +srv := &http.Server{ + Addr: ":8080", + Handler: r, // 把 Engine 作为 Handler 传入 + // 可自定义超时、TLS 等 + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, +} +srv.ListenAndServe() +``` + +> **提问:** 如果用 `http.Server` 配置了 `ReadTimeout`,这个超时作用在 Gin 的哪个阶段?如果请求体很大且读取很慢,Gin 的 `c.Request` 还能拿到完整的 body 吗? + +### 3. 这意味着什么? + +三层含义: + +**① Gin 不是另起炉灶** + +Gin 没有绕开 `net/http` 自己监听端口,而是成为标准的 `http.Handler`,站在标准库的肩膀上做增强。 + +**② 标准库知识完全通用** + +``` +标准库知识 → 完全适用于 Gin +Gin 知识 → 不等同于懂标准库 +``` + +你在标准库里学的中间件模式(`http.Handler` 包装 `http.Handler`),可以直接套用到 Gin 上。反过来则不成立——你会 Gin 不代表你懂 `net/http`,因为 Gin 隐藏了底层的细节。 + +**③ 可以与标准库组件自由组合** + +```go +r := gin.Default() + +// 标准库中间件也可以用在 Gin 上 +r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + log.Printf("[%s] %s", req.Method, req.URL.Path) + next.ServeHTTP(w, req) + }) +}) + +r.GET("/ping", func(c *gin.Context) { + c.JSON(200, gin.H{"message": "pong"}) +}) +r.Run() +``` + +> **提问:** 标准库中间件接收的是 `http.Handler`,而 Gin 中间件接收的是 `*gin.Context`。两者能不能混用?混用时的执行顺序是怎样的? + +### 4. Gin 的 ServeHTTP 做了什么 + +当 `ServeHTTP` 被标准库调用时,它执行了完整的请求处理流程: + +```go +// gin/engine.go — 简化版 +func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // 1. 从 sync.Pool 取 Context + c := engine.getContext() + c.Reset() + c.Request = r + c.writermem.reset(w) + + // 2. 按方法找到对应的 Radix Tree,匹配路径 + handlers, params, _ := engine.tree.match(r.URL.Path, r.Method) + + // 3. 分配 handler 链 + c.handlers = handlers + c.params = params + c.index = -1 + + // 4. 执行中间件链 + c.Next() + + // 5. 归还 Context 到 pool + engine.freeContext(c) +} +``` + +关键链条:**标准库调用 `ServeHTTP` → Gin 做路由匹配 → 执行中间件链 → 写入响应**。 + +> **思考题:** `ServeHTTP` 的 `w` 参数是标准库的 `http.ResponseWriter`,但 Gin 内部用 `responseWriter` 做了包装(加了缓冲)。这意味着响应数据是先写入了 Gin 的缓冲区,再由 `responseWriter` 统一 flush 到标准库的 `w`。这种设计有什么好处?如果 handler 里直接调用 `w.WriteHeader(500)`,Gin 的缓冲机制还能正常工作吗? + +### 5. 核心结论 + +Gin 不是另一个 HTTP 框架,它是 `net/http` 的**增强层**: + +``` +net/http 提供 ──┬── Server、Listener、ResponseWriter + ├── Request、Response、Header + └── ServeMux 路由 + +Gin 替代/增强 ──┬── ServeMux → Radix Tree 路由 + ├── ResponseWriter → buffered writer + ├── Request → gin.Context 包装 + └── Handler → 中间件链 + 绑定 + 渲染 +``` + +所有标准库的知识完全适用于 Gin,反之则不然——你会 Gin 不代表你懂 `net/http`。 + +## 关联笔记 + +- [[1-gin-architecture]] — Gin 整体架构 +- [[middleware]] — 中间件机制 +- [[context-lifecycle]] — Context 的底层设计 diff --git a/hzh/GIN/10-template-rendering.md b/hzh/GIN/10-template-rendering.md new file mode 100644 index 0000000..50baf9b --- /dev/null +++ b/hzh/GIN/10-template-rendering.md @@ -0,0 +1,312 @@ +--- +tags: [后端, Go, Gin, 模板, HTML] +create time: 2026-04-28 00:05 +--- + +# HTML 模板渲染 + +## 概述 + +Gin 内建了对 Go 标准库 `html/template` 的封装,支持简单模板加载、多模板引擎配置、以及通过 `embed.FS` 将模板打包进单一二进制。虽然现代前后端分离架构中较少直接使用服务端渲染,但在管理后台、邮件模板等场景中仍然实用。 + +思考题:`c.HTML` 和 `http.ServeFile` 直接返回 `.html` 文件有什么区别? + +## 正文 + +### 1. 基本模板渲染 + +Gin 通过 `gin.H`(即 `map[string]any`)将数据传给 Go 标准库 `html/template`。Gin 内部调用 `template.ParseFiles()` 加载模板,并以**文件名(不带路径)**作为模板名建立映射: + +```go +func main() { + r := gin.Default() + + // 加载 templates/ 目录下所有 .html 文件 + r.LoadHTMLGlob("templates/*") + + r.GET("/index", func(c *gin.Context) { + // 渲染 templates/index.html,传入模板数据 + // Gin 模板的 key 使用大驼峰(类似结构体字段),Go 的 html/template 按反射访问 + c.HTML(http.StatusOK, "index", gin.H{ + "Title": "Home Page", + "User": "wonder", + "Items": []string{"item1", "item2"}, + }) + }) + + r.GET("/news", func(c *gin.Context) { + c.HTML(http.StatusOK, "news.html", gin.H{ + "Title": "News", + "Items": []string{"item1", "item2"}, + }) + }) + + r.Run(":8080") +} +``` + +模板文件 `templates/index.html`: + +```html + + +{{.Title}} + +

Welcome, {{.User}}

+ + {{range .Items}} +
  • {{.}}
  • + {{end}} + + +``` + +> [!TIP] Gin 模板的 key 命名约定 +> Gin 模板通过 Go 反射访问字段,因此 `gin.H` 中的 key 应使用**大驼峰**(如 `"Title"`),与 HTML 模板中的 `{{.Title}}` 对应。如果写成小写 key `{"title": ...}`,模板中使用 `{{.Title}}` 将无法找到该字段。 + +> [!INFO] 思考题答案 +> `c.HTML` vs `http.ServeFile`:**本质区别是"渲染"还是"返回"**。`c.HTML` 先将数据注入模板执行一次模板引擎渲染(可以做条件判断、循环遍历等),最终输出完整 HTML;`http.ServeFile` 原样发送文件内容,无法动态插入数据。如果要返回静态页面(如前端 SPA 入口),用 `ServeFile` / `StaticFile` 更高效。 + +### 2. `LoadHTMLGlob` vs `LoadHTMLFiles` + +| 方法 | 用途 | 示例 | +|------|------|------| +| `LoadHTMLGlob(pattern)` | 按 glob 模式加载一批模板 | `"templates/**/*.html"` | +| `LoadHTMLFiles(paths...)` | 指定具体的文件列表 | `"templates/base.html", "templates/index.html"` | + +Gin 加载模板后会在内部建立**文件名 → `*template.Template`** 的映射关系,因此 `c.HTML()` 第二个参数只需匹配文件名即可。 + +```go +// Glob — 适合模板较多、结构简单的场景 +r.LoadHTMLGlob("templates/**/*") // 包括子目录 + +// Files — 适合明确知道有哪些模板的场景 +r.LoadHTMLFiles( + "templates/base.html", + "templates/index.html", + "templates/error.html", +) +``` + +> [!WARNING] 常见陷阱 +> `LoadHTMLGlob("templates/**/*")` 会用每个文件的**基名**作为模板名。如果 `templates/sub/page.html` 也被加载,模板名就是 `sub/page.html`——渲染时需写 `c.HTML(200, "sub/page.html", data)`。建议始终用绝对路径模式(如 `"./templates/*.html"`)避免意外匹配到无关文件。 + +### 3. 模板函数(FuncMap) + +Go 模板不像 Jinja2 那样内置丰富的过滤器,因此提供了 `SetFuncMap` 接口来注册自定义函数。这些函数可以在模板中以 **管道符 `|`** 链式调用: + +```go +func main() { + r := gin.Default() + + r.SetFuncMap(template.FuncMap{ + "formatDate": func(t time.Time) string { + return t.Format("2006-01-02") + }, + "truncate": func(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." + }, + "upper": strings.ToUpper, + }) + + r.LoadHTMLGlob("templates/*") + + r.GET("/article", func(c *gin.Context) { + c.HTML(200, "article", gin.H{ + "Title": "Gin Templating Guide", + "Body": "这是一篇很长的文章...", + "Date": time.Now(), + }) + }) + + r.Run(":8080") +} +``` + +模板中使用: + +```html +

    {{.Title | upper}}

    +

    {{.Body | truncate 50}}

    +发布于 {{.Date | formatDate}} +``` + +> [!WARNING] SetFuncMap 必须先于 LoadHTMLGlob 调用 +> Gin 将 `SetFuncMap` 的设置缓存在内部,因此必须在 `LoadHTMLGlob()` / `LoadHTMLFiles()` **之前**设置。否则注册的函数不会生效,模板中出现自定义管道符时会报 "undefined function" 错误。 + +### 4. 模板继承(Base Template) + +Go 原生模板不支持继承,但可以通过 `define` + `block` + `template` 模拟页面布局系统:`base.html` 定义骨架和可替换区块(block),子模板用 `define` 覆盖对应区块。 + +> [!INFO] block vs define 的区别 +> - `{{block "name" .}}...{{end}}` — 用于 **base.html** 中定义默认内容,子模板可以选择性覆盖 +> - `{{define "name"}}...{{end}}` — 用于 **子模板** 中实现自己的版本来覆盖 base 中的默认内容 +> - 两者配合才能实现"继承"效果 + +```html + + + + + {{block "title" .}}Default{{end}} + + + + {{block "content" .}}{{end}} + + + + + +{{define "title"}}Home{{end}} +{{define "content"}} +

    Welcome

    +

    {{.Message}}

    +{{end}} +``` + +渲染时传入组合后的模板: + +```go +r.LoadHTMLFiles( + "templates/base.html", + "templates/index.html", +) +// LoadHTMLFiles 将多个文件加载到同一个 *template.Template 中, +// 因此 index.html 中的 define 可以找到 base.html 中 block 的定义并合并输出。 +``` + +> [!TIP] 更复杂的场景 → 多模板引擎 +> 如果项目需要按模块隔离不同的模板集(如后台管理一套模板、前台展示一套模板),可以使用第三方库 [gin-contrib/multitemplate](https://github.com/gin-contrib/multitemplate),它允许在同一 Router 下注册多个独立的 `*template.Template` 对象。详见本章「进阶用法」部分。 + +### 5. 将模板打包进单一二进制(Go 1.16+) + +使用 `embed.FS` 把模板文件嵌入 Go 编译产物,适合 Docker 单镜像部署: + +```go +import _ "embed" +import "html/template" + +//go:embed templates/*.html +var templateFS embed.FS + +func main() { + r := gin.Default() + + // Go 1.23+:ParseFS 直接返回 *template.Template,配合 Must 处理错误 + t := template.Must(template.New("").ParseFS(templateFS, "templates/*.html")) + r.SetHTMLTemplate(t) + + r.Run(":8080") +} +``` + +> [!INFO] 原理 +> `SetHTMLTemplate` 替换 Gin 内部的默认模板对象(`*template.Template`),之后所有 `c.HTML()` 调用都走这个已嵌入的模板集。如果只需加载单个新模板而非全部替换,也可以用 `t.AddParseTree("name", tree)` 追加。 + +> [!NOTE] 项目目录结构 +> +> ``` +> cmd/ +> ├── server/main.go ← embed 入口 +> templates/ ← 模板文件,不会被 gitignore +> ├── base.html +> ├── index.html +> └── error.html +> internal/ +> └── handlers/ +> ``` +> +> `embed` 指令位于 `main.go` 所在目录下执行,`templates/` 是相对于 `main.go` 的路径。部署时只需一个二进制文件,不再需要挂载 Volume 或复制模板文件到容器中。 + +### 6. 模板安全注意事项 + +> [!INFO] 安全原则 +> 永远不要手动拼接入用户可控的 HTML 内容。Go 的 `html/template` 包根据**上下文自动选择转义策略**,使用默认字符串类型即可获得最安全的输出。只有在明确需要渲染富文本时才考虑绕过转义。 + +```mermaid +flowchart LR + A["用户输入"] --> B["存入 gin.H 数据"] + B --> C["传入 c.HTML 渲染"] + C --> D{是否 HTML 转义?} + D -->|"是 ✅"| E["自动转义,安全"] + D -->|"否 ❌"| F["XSS 漏洞"] + + style E fill:#e8f5e9 + style F fill:#ffebee +``` + +Go 的 `html/template` 包**自动对上下文相关内容进行转义**: +- 在 HTML body 中 → 转义 `<>&"'` +- 在 attribute 中 → 转义引号和 `<>&` +- 在 JS 上下文中 → 转义 `'` 和 `<\/` + +**唯一例外:** 使用 `template.HTML` / `template.JS` 类型包装的内容不会转义——这意味着你主动告诉模板"这段内容是安全的"。滥用会导致 XSS: + +```go +// ❌ 危险:用户输入未过滤就标为 safe +c.HTML(200, "page", gin.H{ + "content": template.HTML(userInput), // 可能被注入 + +``` + +> [!danger]- JSONP 安全风险与 CORS 替代方案 +> JSONP 的本质是在服务端拼接 JavaScript 代码——如果回调名未做白名单校验,攻击者可以注入恶意脚本: +> +> ```text +> # 恶意请求 +> ?callback= +> +> # 被注入的输出 +> ({"msg":"hello"}) +> ``` +> +> **现代替代方案:** +> +> | 方案 | 优点 | 缺点 | +> |------|------|------| +> | **CORS** (`Access-Control-Allow-Origin: *`) | 安全、灵活、现代浏览器全支持 | 需要预检(OPTIONS)请求 | +> | **JSONP** | 兼容 IE6+ | 仅支持 GET、安全风险高、已淘汰 | +> +> **生产环境强烈建议改用 CORS**,配合 `Access-Control-Allow-Origin` 头使用。 + +### 7. 其他常用渲染方法一览 + +| 方法 | Content-Type | 典型用途 | +|------|-------------|---------| +| `c.String(code, fmt, a...)` | `text/plain; charset=utf-8` | 短文本 / 调试 | +| `c.Data(code, ct, bytes)` | 自定义 | 原始字节流(图片、PDF 等) | +| `c.File(filepath)` | 自动推断 | 静态资源下载 | +| `c.FileBinary(filepath)` | `application/octet-stream` | 二进制文件下载 | +| `c.FileAttachment(filepath, name)` | `application/octet-stream; attachment` | 强制下载弹窗 + 自定义文件名 | +| `c.HTML(code, tmpl, obj)` | `text/html` | HTML 模板渲染 | +| `c.ProtoBuf(code, pb)` | `application/x-protobuf` | Protobuf 序列化 | + +> [!note]- File / FileBinary / FileAttachment 选型 +> | 方法 | Content-Type | 浏览器行为 | +> |------|-------------|-----------| +> | `c.File(filepath)` | 按扩展名自动推断(`.jpg` → `image/jpeg`) | **可能直接在浏览器预览** | +> | `c.FileBinary(filepath)` | `application/octet-stream` | 触发下载,但无自定义文件名 | +> | `c.FileAttachment(filepath, name)` | `application/octet-stream; attachment` | 触发下载 + 指定弹出文件名 | +> +> 如果希望用户下载文件而非在浏览器中打开,优先用 `c.FileAttachment()`。如果需要指定弹窗时显示的文件名(例如 `report-2026Q2.pdf`),这个方法也最方便。 + +### 8. 自定义 CSV 渲染 + +Gin 没有内置 `c.CSV()`,但组合 `c.Data()` 即可轻松实现: + +```go +import ( + "bytes" + "encoding/csv" + "net/http" + + "github.com/gin-gonic/gin" +) + +func csvExport(c *gin.Context) { + var buf bytes.Buffer + w := csv.NewWriter(&buf) + w.Write([]string{"name", "age", "city"}) + w.Write([]string{"Alice", "30", "Beijing"}) + w.Write([]string{"Bob", "25", "Shanghai"}) + w.Flush() + + c.Header("Content-Disposition", "attachment; filename=data.csv") + c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes()) +} +``` + +> **思考题:** 上面几节我们讨论了如何用 `switch` 根据 `Accept` 头做内容协商。如果支持的格式超过三种,每个分支都要写一次 `switch`,比较繁琐。有没有更优雅的封装? + +Gin 提供了 `c.Negotiate()` 方法,将协商逻辑和响应写入合并到一个调用中: + +```go +func negotiateHandler(c *gin.Context) { + data := gin.H{"title": "Go Guide", "version": "1.0"} + + c.Negotiate(http.StatusOK, gin.Negotiate{ + Offered: []string{ + "application/json", + "application/xml", + "application/yaml", + }, + Handler: func() { + switch c.NegotiationFormat() { + case "application/json": + c.JSON(http.StatusOK, data) + case "application/xml": + c.XML(http.StatusOK, data) + case "application/yaml": + c.YAML(http.StatusOK, data) + default: + c.AbortWithError(http.StatusNotAcceptable, + errors.New("unsupported media type")) + } + }, + }) +} +``` + +`c.Negotiate()` 内部会先检查 `Accept` 头是否在 `Offered` 列表中——如果是则进入 `Handler` 写入对应响应;如果不是则从 `Offered` 中选第一个作为默认格式写入,最后自动设置正确的 `Content-Type` 响应头。**推荐将所有多格式接口统一用此模式编写。** + +## 关联笔记 + +- [[BACKEND/GIN/0-overview]] +- [[BACKEND/GIN/2-context-request]] +- [[BACKEND/GIN/1-middleware]] +- [[BACKEND/GIN/3-error-handling]] diff --git a/hzh/GIN/README.md b/hzh/GIN/README.md new file mode 100644 index 0000000..7b62cd8 --- /dev/null +++ b/hzh/GIN/README.md @@ -0,0 +1,98 @@ +--- +tags: [后端, Go, Gin, 索引] +create time: 2026-04-27 00:00 +--- + +# Gin 框架学习笔记 + +## 概述 + +本文件夹为 Go 语言 Gin 框架的系统学习笔记。`[[Go 后端基础]]` 已涵盖 Gin 的快速上手(路由分组、中间件、参数绑定、错误处理),本笔记群在此基础上**深入和拓展**,聚焦 Gin 框架特有的机制和工程实践。 + +本索引以 [Gin 官方文档](https://gin-gonic.com/zh-cn/docs/) 目录为骨架,结合教学逻辑重新组织——**不是逐条翻译官网,而是把相关联的知识点合并成体系化的笔记**。 + +## 笔记索引 + +### 一、核心机制(必读) + +> 这些笔记帮你理解 Gin 的底层工作原理,是读懂源码和排查问题的基础。 + +| 序号 | 笔记 | 官网对照 | 内容概要 | +| --- | ------------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | [[1-gin-architecture]] | 介绍 + 快速开始 | 整体架构:Engine、RouterGroup、Context 的关系;请求生命周期全链路(Mermaid 时序图);Gin 如何桥接 `net/http` | +| 2 | [[2-routing]] | 路由 + 路由分组 + 重定向 | 路由匹配算法(Radix Tree 基数树);静态/动态/通配符路由的优先级;路由分组与前缀累加原理;重定向 `c.Redirect` | +| 3 | [[3-middleware]] | 使用中间件 + 自定义中间件 + 中间件中的 Goroutine + 安全头 | 中间件链执行顺序(全局 → 分组 → 路由);`gin.HandlerFunc` 的本质;常用中间件实现模板(CORS、限流、鉴权、RequestID、安全头);中间件中启动 Goroutine 的陷阱与 `c.Copy()` 用法 | +| 4 | [[4-context-lifecycle]] | 上下文与取消 | `*gin.Context` 底层设计:keys/values map、Request/RW 包装;`c.Set/Get/GetString`;`c.Copy()` 的深拷贝边界;请求级 `context` 传播与取消 | +| 5 | [[5-binding-validation]] | 模型绑定和验证 + 自定义验证器 + 绑定查询字符串 + 绑定自定义反序列化器 + 绑定请求头 + 绑定 URI + 绑定 HTML 复选框 | `ShouldBind` 全家桶(JSON、form、query、header、uri);`binding` 标签内置规则速查;自定义 Validator(`.RegisterValidation`);自定义反序列化器(`bind.DeferredBinder`);数组集合格式(`UserIds[]`) | + + +### 二、进阶功能 + +> 这些是日常开发中高频使用、但容易踩坑的场景。 + +| 序号 | 笔记 | 官网对照 | 内容概要 | +|------|------|----------|----------| +| 6 | [[6-error-handling]] | 错误处理中间件 | `c.Error` → `c.Errors` 链式错误收集;全局错误处理器 `gin.Recovery` 定制;HTTP 状态码与业务码的映射;统一错误响应中间件 | +| 7 | [[7-binding-advanced]] | Multipart/Urlencoded 表单 + Map 作为参数 + 绑定查询字符串或 POST 数据 + 表单默认值 + 使用自定义结构体标签绑定 + 将请求体绑定到不同的结构体 + 数据绑定 | 表单绑定深入:`c.ShouldBind()` 的多内容类型自动检测;Map 绑定(`binding:"-"` 跳过字段);查询参数与 POST body 混合绑定;字段默认值策略;按条件绑定不同结构体(`ShouldBindBodyWith`) | +| 8 | [[8-file-upload]] | 文件上传(单文件/多文件/限制大小) | `c.ShouldBindFiles`;单文件/多文件上传流程;`MaxMultipartMemory` 内存限制;文件类型/大小校验;分片上传思路 | +| 9 | [[9-response-rendering]] | XML/JSON/YAML/ProtoBuf 渲染 + SecureJSON + JSONP + AsciiJSON + 渲染 + PureJSON | 渲染全家桶:`c.JSON`、`c.XML`、`c.YAML`、`c.ProtoBuf`;`SecureJSON`(防 JSON 劫持);`PureJSON`(保留原始 Unicode);`AsciiJSON`(中文转 Unicode);`JSONP` | +| 10 | [[10-template-rendering]] | HTML 渲染 + 多模板 + 将模板构建到单一二进制中 | `c.HTML` / `LoadHTMLGlob` / `LoadHTMLFiles`;多模板(`Template.FuncMap`);`embed.FS` 将模板打包进二进制 | +| 11 | [[11-static-files]] | 提供静态文件 + 从文件提供数据 + 从 Reader 提供数据 | `Static` / `StaticFS` / `StaticFile`;自定义文件服务器;`io.Reader` 直接返回文件流 | + +### 三、服务器与部署 + +> 涉及 Gin 服务器的配置、运行方式和部署策略。 + +| 序号 | 笔记 | 官网对照 | 内容概要 | +|------|------|----------|----------| +| 12 | [[12-server-config]] | 自定义 HTTP 配置 + 服务器配置 + 支持 Let's Encrypt + Cookie + 可信代理 | `gin.New()` 自定义 Engine;`http.Server` 高级配置(超时、KeepAlive);TLS/Let's Encrypt;Cookie 操作(`c.SetCookie` / `c.GetCookie`);可信代理链(X-Forwarded-For) | +| 13 | [[13-graceful-shutdown]] | 优雅重启或停止 | `server.Shutdown()` + 信号监听(SIGINT/SIGTERM);等待请求处理完毕再退出;优雅重启(fork + exec)思路 | +| 14 | [[14-logging]] | 如何写入日志文件 + 自定义日志格式 + 跳过日志记录 + 控制输出着色 + 避免记录查询字符串 + 定义路由日志格式 + 日志 + 结构化日志 | 日志器替换(`gin.DefaultWriter`);自定义日志格式;结构化日志(zap/logr 接入);跳过特定路径日志;路由日志格式定制 | +| 15 | [[15-advanced-running]] | 运行多个服务 + HTTP/2 服务器推送 | 单进程多监听端口;gRPC + HTTP 共存;HTTP/2 push 场景 | + +### 四、工程实践 + +> 把 Gin 用到生产级别的实践。 + +| 序号 | 笔记 | 官网对照 | 内容概要 | +|------|------|----------|----------| +| 16 | `project-structure.md` | 依赖注入模式 | 标准项目目录结构(cmd/internal/handler/service/model/repository);模块化路由注册;依赖注入模式(手动 vs 依赖注入容器) | +| 17 | `testing.md` | 测试 | `httptest` + `github.com/gin-gonic/gin/test`;Mock `*gin.Context`;中间件单独测试;基准测试(`Benchmark`) | +| 18 | `observability.md` | 健康检查 + 指标与监控 | `/health`、`/ready`、`/metrics` 端点;Prometheus 指标接入;链路追踪(OpenTelemetry) | +| 19 | `websocket.md` | WebSocket 支持 | Gin + gorilla/websocket 集成;Upgrade 握手;读写超时控制;广播推送 | +| 20 | `session-auth.md` | 会话管理 | Cookie Session 实现;JWT 认证中间件;RBAC 权限控制中间件 | +| 21 | `grpc-gateway.md` | — | Gin 作为 gRPC 服务的 HTTP 网关(gRPC-Gateway 原理) | + +### 五、构建与优化 + +| 序号 | 笔记 | 官网对照 | 内容概要 | +|------|------|----------|----------| +| 22 | `build-and-perf.md` | 使用 JSON 替换构建 + 不使用 MsgPack 构建 + 构建标签 + 基准测试 | 构建标签(`// +build`);替换 JSON 编码器(json-iterator);禁用 MsgPack;基准测试编写与解读 | + +## 已创建笔记 + +> 快速跳转链接:`[[1-gin-architecture]]` · `[[2-routing]]` · `[[3-middleware]]` · `[[4-context-lifecycle]]` · `[[5-binding-validation]]` · `[[6-error-handling]]` · `[[7-binding-advanced]]` · `[[8-file-upload]]` · `[[9-response-rendering]]` · `[[10-template-rendering]]` · `[[11-static-files]]` · `[[12-server-config]]` · `[[13-graceful-shutdown]]` · `[[14-logging]]` · `[[15-advanced-running]]` + +--- + +- `[[Go 后端基础]]` — Gin 快速上手,已涵盖基础用法 +- `[[HTTP 协议]]` — HTTP 协议基础 +- `[[API 设计]]` — API 设计规范与错误码体系 +- `[[数据库基础]]` — Go 操作数据库 +- `[[部署与运维基础]]` — Go 应用部署 + +## 学习路线建议 + +``` +快速上手 (Go 后端基础) + ↓ +核心机制 (序号1-5) ← 读懂源码、排查问题的关键 + ↓ +工程实践 (序号16-21) ← 项目实战必备 + ↓ +进阶功能 (序号6-15) ← 按需深入 + ↓ +构建与优化 (序号22) ← 性能调优阶段 +``` + +共 **22 篇笔记**,已创建 16 篇(核心机制 + 进阶功能 + 服务器与部署),6 篇待完成(工程实践 #16-21、构建与优化 #22)。