diff --git a/BACKEND/GIN/10-template-rendering.md b/BACKEND/GIN/10-template-rendering.md
new file mode 100644
index 0000000..92c9028
--- /dev/null
+++ b/BACKEND/GIN/10-template-rendering.md
@@ -0,0 +1,239 @@
+---
+tags: [后端, Go, Gin, 模板, HTML]
+create time: 2026-04-28 00:00
+---
+
+# HTML 模板渲染
+
+## 概述
+
+Gin 内建了对 Go 标准库 `html/template` 的封装,支持简单模板加载、多模板引擎配置、以及通过 `embed.FS` 将模板打包进单一二进制。虽然现代前后端分离架构中较少直接使用服务端渲染,但在管理后台、邮件模板等场景中仍然实用。
+
+思考题:`c.HTML` 和 `http.ServeFile` 直接返回 `.html` 文件有什么区别?
+
+## 正文
+
+### 1. 基本模板渲染
+
+```go
+func main() {
+ r := gin.Default()
+
+ // 加载 templates/ 目录下所有 .html 文件
+ r.LoadHTMLGlob("templates/*")
+
+ r.GET("/index", func(c *gin.Context) {
+ // 渲染 templates/index.html,传入模板数据
+ c.HTML(http.StatusOK, "index", gin.H{
+ "title": "Home Page",
+ "user": "wonder",
+ })
+ })
+
+ r.GET("/news", func(c *gin.Context) {
+ c.HTML(http.StatusOK, "news.tmpl", gin.H{
+ "title": "News",
+ "items": []string{"item1", "item2"},
+ })
+ })
+
+ r.Run(":8080")
+}
+```
+
+模板文件 `templates/index.html`:
+
+```html
+
+
+
{{.Title}}
+
+ Welcome, {{.User}}
+
+ {{range .Items}}
+ {{.}}
+ {{end}}
+
+
+```
+
+> **关键点:** 第二个参数是模板文件名(不带路径),Gin 根据 `LoadHTMLGlob` 或 `LoadHTMLFiles` 配置的映射来查找。
+
+### 2. `LoadHTMLGlob` vs `LoadHTMLFiles`
+
+| 方法 | 用途 | 示例 |
+|------|------|------|
+| `LoadHTMLGlob(pattern)` | 按 glob 模式加载一批模板 | `"templates/**/*.html"` |
+| `LoadHTMLFiles(paths...)` | 指定具体的文件列表 | `"templates/base.html", "templates/index.html"` |
+
+```go
+// Glob — 适合模板较多、结构简单的场景
+r.LoadHTMLGlob("templates/**/*") // 包括子目录
+
+// Files — 适合明确知道有哪些模板的场景
+r.LoadHTMLFiles(
+ "templates/base.html",
+ "templates/index.html",
+ "templates/error.html",
+)
+```
+
+> **陷阱:** `LoadHTMLGlob("templates/**/*")` 会用每个文件的**基名**作为模板名。如果 `templates/sub/page.html` 也被加载,模板名就是 `sub/page.html`——渲染时需写 `c.HTML(200, "sub/page.html", data)`。
+
+### 3. 多模板引擎(Template.FuncMap)
+
+可以注册自定义模板函数,类似 Python Jinja2 的 filter:
+
+```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}}
+```
+
+### 4. 模板继承(Base Template)
+
+Go 原生模板不支持继承,但可以通过 `define` + `template` 模拟:
+
+```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",
+)
+// 此时 index.html 中的 {{template "base"}} 才能找到定义
+```
+
+### 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()
+
+ // 从 embedded FS 加载模板
+ r.LoadHTMLTemplates(template.New("").ParseFS(templateFS, "templates/*.html"))
+
+ r.Run(":8080")
+}
+```
+
+```go
+// Go 1.23+ 写法
+r.LoadHTMLTemplates(template.Must(template.New("").ParseFS(templateFS, "templates/*.html")))
+```
+
+**项目目录结构:**
+
+```
+cmd/
+├── server/main.go ← embed 入口
+templates/ ← 模板文件,不会被 gitignore
+├── base.html
+├── index.html
+└── error.html
+internal/
+└── handlers/
+```
+
+> **优势:** 部署时只需一个二进制文件,不再需要挂载 Volume 或复制模板文件到容器中。
+
+### 6. 模板安全注意事项
+
+```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), // 可能被注入
+
+```
+
+> **安全警告:** JSONP 要求回调函数名白名单校验,否则攻击者可构造 `callback=` 注入 XSS。
+
+### 7. 渲染方法速查
+
+| 方法 | 内容类型 | 特点 |
+|------|----------|------|
+| `c.JSON(code, obj)` | `application/json` | 标准 JSON,ASCII 转义 |
+| `c.PureJSON(code, obj)` | `application/json` | 保留原始 Unicode |
+| `c.SecureJSON(code, obj)` | `application/json` | 加前缀防劫持 |
+| `c.AsciiJSON(code, obj)` | `application/json` | 强制 ASCII 编码 |
+| `c.XML(code, obj)` | `application/xml` | XML 序列化 |
+| `c.YAML(code, obj)` | `application/x-yaml` | YAML 序列化 |
+| `c.ProtoBuf(code, obj)` | `application/x-protobuf` | Protobuf 序列化 |
+| `c.String(code, format, vals)` | `text/plain` | 字符串格式化 |
+| `c.Data(code, data)` | 自定义 | 原始字节流 |
+| `c.HTML(code, tmplName, obj)` | `text/html` | HTML 模板渲染 |
+
+思考题:如果你的 API 同时需要提供 JSON 和 CSV 两种格式,Gin 本身没有 `c.CSV()`,你应该怎么做?
+
+提示:`c.Data()` 和 `c.Writer.Write()` 的组合。
diff --git a/BACKEND/GIN/README.md b/BACKEND/GIN/README.md
index 6690d97..7b62cd8 100644
--- a/BACKEND/GIN/README.md
+++ b/BACKEND/GIN/README.md
@@ -17,14 +17,14 @@ create time: 2026-04-27 00:00
> 这些笔记帮你理解 Gin 的底层工作原理,是读懂源码和排查问题的基础。
-| 序号 | 笔记 | 官网对照 | 内容概要 |
-|------|------|----------|----------|
-| 1 | `gin-architecture.md` | 介绍 + 快速开始 | 整体架构:Engine、RouterGroup、Context 的关系;请求生命周期全链路(Mermaid 时序图);Gin 如何桥接 `net/http` |
-| 2 | `routing.md` | 路由 + 路由分组 + 重定向 | 路由匹配算法(Radix Tree 基数树);静态/动态/通配符路由的优先级;路由分组与前缀累加原理;重定向 `c.Redirect` |
-| 3 | `middleware.md` | 使用中间件 + 自定义中间件 + 中间件中的 Goroutine + 安全头 | 中间件链执行顺序(全局 → 分组 → 路由);`gin.HandlerFunc` 的本质;常用中间件实现模板(CORS、限流、鉴权、RequestID、安全头);中间件中启动 Goroutine 的陷阱与 `c.Copy()` 用法 |
-| 4 | `context-lifecycle.md` | 上下文与取消 | `*gin.Context` 底层设计:keys/values map、Request/RW 包装;`c.Set/Get/GetString`;`c.Copy()` 的深拷贝边界;请求级 `context` 传播与取消 |
-| 5 | `binding-validation.md` | 模型绑定和验证 + 自定义验证器 + 绑定查询字符串 + 绑定自定义反序列化器 + 绑定请求头 + 绑定 URI + 绑定 HTML 复选框 | `ShouldBind` 全家桶(JSON、form、query、header、uri);`binding` 标签内置规则速查;自定义 Validator(`.RegisterValidation`);自定义反序列化器(`bind.DeferredBinder`);数组集合格式(`UserIds[]`) |
-| 6 | `handler-relationship.md` | — | Gin 与 `http.Handler` 的关系:`*gin.Engine` 实现 `http.Handler` 接口的含义;等价于 `http.ListenAndServe` 启动;标准库与 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[]`) |
+
### 二、进阶功能
@@ -32,12 +32,12 @@ create time: 2026-04-27 00:00
| 序号 | 笔记 | 官网对照 | 内容概要 |
|------|------|----------|----------|
-| 6 | `error-handling.md` | 错误处理中间件 | `c.Error` → `c.Errors` 链式错误收集;全局错误处理器 `gin.Recovery` 定制;HTTP 状态码与业务码的映射;统一错误响应中间件 |
-| 7 | `binding-advanced.md` | Multipart/Urlencoded 表单 + Map 作为参数 + 绑定查询字符串或 POST 数据 + 表单默认值 + 使用自定义结构体标签绑定 + 将请求体绑定到不同的结构体 + 数据绑定 | 表单绑定深入:`c.ShouldBind()` 的多内容类型自动检测;Map 绑定(`binding:"-"` 跳过字段);查询参数与 POST body 混合绑定;字段默认值策略;按条件绑定不同结构体(`ShouldBindBodyWith`) |
-| 8 | `file-upload.md` | 文件上传(单文件/多文件/限制大小) | `c.ShouldBindFiles`;单文件/多文件上传流程;`MaxMultipartMemory` 内存限制;文件类型/大小校验;分片上传思路 |
-| 9 | `response-rendering.md` | XML/JSON/YAML/ProtoBuf 渲染 + SecureJSON + JSONP + AsciiJSON + 渲染 + PureJSON | 渲染全家桶:`c.JSON`、`c.XML`、`c.YAML`、`c.ProtoBuf`;`SecureJSON`(防 JSON 劫持);`PureJSON`(保留原始 Unicode);`AsciiJSON`(中文转 Unicode);`JSONP` |
-| 10 | `template-rendering.md` | HTML 渲染 + 多模板 + 将模板构建到单一二进制中 | `c.HTML` / `LoadHTMLGlob` / `LoadHTMLFiles`;多模板(`Template.FuncMap`);`embed.FS` 将模板打包进二进制 |
-| 11 | `static-files.md` | 提供静态文件 + 从文件提供数据 + 从 Reader 提供数据 | `Static` / `StaticFS` / `StaticFile`;自定义文件服务器;`io.Reader` 直接返回文件流 |
+| 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` 直接返回文件流 |
### 三、服务器与部署
@@ -45,10 +45,10 @@ create time: 2026-04-27 00:00
| 序号 | 笔记 | 官网对照 | 内容概要 |
|------|------|----------|----------|
-| 12 | `server-config.md` | 自定义 HTTP 配置 + 服务器配置 + 支持 Let's Encrypt + Cookie + 可信代理 | `gin.New()` 自定义 Engine;`http.Server` 高级配置(超时、KeepAlive);TLS/Let's Encrypt;Cookie 操作(`c.SetCookie` / `c.GetCookie`);可信代理链(X-Forwarded-For) |
-| 13 | `graceful-shutdown.md` | 优雅重启或停止 | `server.Shutdown()` + 信号监听(SIGINT/SIGTERM);等待请求处理完毕再退出;优雅重启(fork + exec)思路 |
-| 14 | `logging.md` | 如何写入日志文件 + 自定义日志格式 + 跳过日志记录 + 控制输出着色 + 避免记录查询字符串 + 定义路由日志格式 + 日志 + 结构化日志 | 日志器替换(`gin.DefaultWriter`);自定义日志格式;结构化日志(zap/logr 接入);跳过特定路径日志;路由日志格式定制 |
-| 15 | `advanced-running.md` | 运行多个服务 + HTTP/2 服务器推送 | 单进程多监听端口;gRPC + HTTP 共存;HTTP/2 push 场景 |
+| 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 场景 |
### 四、工程实践
@@ -69,7 +69,11 @@ create time: 2026-04-27 00:00
|------|------|----------|----------|
| 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 协议基础
@@ -91,4 +95,4 @@ create time: 2026-04-27 00:00
构建与优化 (序号22) ← 性能调优阶段
```
-共 **22 篇笔记**,其中 16 篇高优先级(序号1-11、16-21),6 篇按需展开(序号12-15、22)。
+共 **22 篇笔记**,已创建 16 篇(核心机制 + 进阶功能 + 服务器与部署),6 篇待完成(工程实践 #16-21、构建与优化 #22)。
diff --git a/CLAUDE.md b/CLAUDE.md
index bbed836..72c8685 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -7,7 +7,7 @@
在初始化和完成文件时,必须**完整**读取参照 `./config/agent/DOCUMENT_OPERATION.md
了解结构、规范和惯例。
-如果用户提及创建子文档,意思为创建同名子文件夹,在子文件夹下创建文档,父文档中适当位置插入新文档链接。
+如果用户提及创建子文档,意思为在同一目录下先创建该父文件的同名子文件夹,再在子文件夹下创建文档,最后父文档中适当位置插入新文档链接。
在初次完善后,读取文件并二次检查是否符合文档的每一条内容规范——
- **教学者模式** 提问设计
- **代码示例** 合理注释
diff --git a/GIN/3-middleware/cors-preflight.md b/GIN/3-middleware/cors-preflight.md
new file mode 100644
index 0000000..e69de29
diff --git a/TEST/1-gin-review-quiz.md b/TEST/1-gin-review-quiz.md
new file mode 100644
index 0000000..a02f190
--- /dev/null
+++ b/TEST/1-gin-review-quiz.md
@@ -0,0 +1,715 @@
+---
+tags: [后端, Go, Gin, 复习, 测试]
+create time: 2026-04-28
+---
+
+# Gin 框架复习题库(Level 1→5)
+
+## 使用说明
+
+本题库覆盖 `[[GIN/1-gin-architecture]]` ~ `[[GIN/5-binding-validation]]` 全部核心知识点,共 **30 道题**:
+
+| 题型 | 数量 | 每题分 | 合计 |
+|------|------|--------|------|
+| 选择题(单选) | 15 题 | 4 分 | 60 分 |
+| 填空题 | 8 题 | 5 分 | 40 分 |
+| 代码补全 | 7 题 | 约 6 分 | ~42 分 |
+
+**总分:约 142 分。建议用时 60~90 分钟。**
+
+---
+
+## 一、选择题(每题 4 分,共 60 分)
+
+### Q1 【架构】`gin.Default()` 默认挂载了哪两个中间件?
+
+A. Logger + CORS
+B. Logger + Recovery
+C. Recovery + JWT Auth
+D. Logger + RateLimit
+
+点击查看答案
+
+**答案:B**。`gin.Default()` = `gin.New()` + `Use(Logger())` + `Use(Recovery())`。
+
+> 来源:`[[GIN/1-gin-architecture]]` §4
+
+
+
+---
+
+### Q2 【架构/Context】Gin 的 `*gin.Engine` 实现了标准库中哪个接口,从而可以无缝传给 `http.ListenAndServe`?
+
+A. `http.RoundTripper`
+B. `http.Handler`
+C. `http.ResponseWriter`
+D. `http.ServeMux`
+
+点击查看答案
+
+**答案:B**。`ServeHTTP(ResponseWriter, *Request)` 签名匹配。
+
+> 来源:`[[GIN/1-gin-architecture/engine-handler]]` §1
+
+
+
+---
+
+### Q3 【路由】Gin 的路由匹配算法使用什么数据结构?
+
+A. `map[string]Handler`
+B. Radix Tree(基数树/压缩前缀树)
+C. AVL Tree(平衡二叉树)
+D. Trie(朴素前缀树,未压缩)
+
+点击查看答案
+
+**答案:B**。Radix Tree 通过前缀共享实现 O(d) 匹配,d 为 URL 深度。
+
+> 来源:`[[GIN/2-routing]]` §3;`[[GIN/2-routing-complexity-comparison]]`
+
+
+
+---
+
+### Q4 【路由优先级】当同时注册 `/users/list`(静态)、`/users/:id`(动态)、`/users/*path`(通配符)时,请求 `/users/5` 会命中哪个路由?
+
+A. `/users/list` — 因为它是第一个注册的
+B. `/users/:id` — 静态优先于动态
+C. `/users/*path` — 通配符是兜底规则
+D. 取决于注册顺序
+
+点击查看答案
+
+**答案:B**。优先级:静态 > 动态参数 > 通配符,与注册顺序无关。
+
+> 来源:`[[GIN/2-routing]]` §4
+
+
+
+---
+
+### Q5 【路由分组】以下代码输出的完整路径是什么?
+
+```go
+r := gin.Default()
+api := r.Group("/api")
+v1 := api.Group("/v1")
+users := v1.Group("/users/:id")
+users.GET("", handler)
+```
+
+A. `/api/v1/users/:id`
+B. `/api/v1/users/` (`:id` 被忽略,因为放在 Group path 里)
+C. `/api/v1/users/:id` — 正确
+D. `/:id` — 只取最后一段
+
+点击查看答案
+
+**答案:C**。`basePath` 逐层累加:`"" → "/api" → "/api/v1" → "/api/v1/users/:id"`。
+
+> 来源:`[[GIN/2-routing]]` §5
+
+
+
+---
+
+### Q6 【路由复杂度对比】如果有 10000 条路由,用 Gin 的 Radix Tree 和 `http.ServeMux` 分别匹配一个 URL,大致需要多少步?
+
+A. Gin: 10000 步,ServeMux: 10000 步
+B. Gin: ~4 步,ServeMux: ~10000 次比较
+C. Gin: ~10000 步,ServeMux: ~4 步
+D. Gin: ~4 步,ServeMux: ~4 步
+
+点击查看答案
+
+**答案:B**。Gin 复杂度 O(d),与路由总数 R 无关;ServeMux O(R×L)。
+
+> 来源:`[[GIN/2-routing-complexity-comparison]]` §3
+
+
+
+---
+
+### Q7 【中间件执行顺序】注册了两个全局中间件 A 和 B(A 先注册),一个 handler。完整的输出顺序是?
+
+```go
+// A: fmt.Println("A-before"); c.Next(); fmt.Println("A-after")
+// B: fmt.Println("B-before"); c.Next(); fmt.Println("B-after")
+// handler: fmt.Println("handler")
+```
+
+A. A-before → B-before → handler → B-after → A-after
+B. A-before → B-before → handler → A-after → B-after
+C. A-before → handler → A-after → B-before → B-after
+D. handler → A-before → B-before → A-after → B-after
+
+点击查看答案
+
+**答案:A**。前置按注册顺序,后置按逆序——栈式行为。
+
+> 来源:`[[GIN/3-middleware]]` §2
+
+
+
+---
+
+### Q8 【中间件 vs 标准库】Go 标准库 `net/http` 中间件的类型签名是?
+
+A. `func(*gin.Context)`
+B. `func(http.ResponseWriter, *http.Request)`
+C. `func(http.Handler) http.Handler`
+D. `func(*http.Server) http.Handler`
+
+点击查看答案
+
+**答案:C**。这是装饰器模式,层层嵌套包装。
+
+> 来源:`[[GIN/gin-vs-std]]` §1
+
+
+
+---
+
+### Q9 【c.Abort()】中间件 A 中调用了 `c.Abort()`(紧接 `return`),以下说法正确的是?
+
+A. 后续中间件和 handler 都跳过,但 A 的后置逻辑会执行
+B. 后续中间件跳过,handler 执行
+C. 整个链(包括后续中间件、handler、A 的所有剩余代码)都不再执行
+D. 只有同一路由的中间件被跳过,其他路由不受影响
+
+点击查看答案
+
+**答案:C**。`c.Abort()` 将 index 设为链长度,`c.Next()` 循环条件立即不满足。Abort 后必须紧跟 return。
+
+> 来源:`[[GIN/middleware-abort]]`
+
+
+
+---
+
+### Q10 【中间件 Goroutine】在中间件中启动 goroutine 异步处理日志,正确的做法是?
+
+A. 直接在 goroutine 中使用 `c`
+B. 先用 `c.Copy()` 创建副本,在 goroutine 中使用副本
+C. 把 `c` 保存到全局变量,在 goroutine 中读取
+D. 使用 `context.WithCancel` 取消原 context
+
+点击查看答案
+
+**答案:B**。`c.Copy()` 创建独立副本,goroutine 中只能读不能写响应。
+
+> 来源:`[[GIN/3-middleware]]` §5
+
+
+
+---
+
+### Q11 【Context 池化】如果在 handler 中把 `*gin.Context` 保存到全局变量,下次请求时会发生什么?
+
+A. 读到的是上一次请求的数据,完全安全
+B. 可能读到任意并发请求正在使用的数据,造成数据错乱
+C. Go 运行时会 panic,因为存在数据竞争
+D. Context 会自动深拷贝,所以没问题
+
+点击查看答案
+
+**答案:B**。sync.Pool 复用对象,字段被 reset 覆盖,全局引用指向的是被新请求改写后的同一个内存地址。
+
+> 来源:`[[GIN/context-pool]]` §2
+
+
+
+---
+
+### Q12 【Context reset】Context 从 pool 取出后,`reset()` 方法把 `index` 重置为多少?为什么?
+
+A. `0` — 表示从头开始
+B. `-1` — 表示还未开始执行,第一次 `c.Next()` 走到 index+1=0
+C. `-1` — 表示无效值,需要用 -1 做判断
+D. `nil` — 空表示未初始化
+
+点击查看答案
+
+**答案:B**。`-1` 表示还未开始,`c.Next()` 先 `index++` 到 0,再执行 `handlers[0]`。
+
+> 来源:`[[GIN/4-context-lifecycle]]` §3
+
+
+
+---
+
+### Q13 【超时控制】Gin 默认是否有 HTTP 请求超时控制?如果需要超时,应该在哪里配置?
+
+A. 有,默认 30 秒超时
+B. 没有,应在 `http.Server` 层配置 `ReadTimeout`/`WriteTimeout`
+C. 没有,但可用 `c.WithTimeout()` 设置
+D. 有,在 `gin.Default()` 内部已经设置了
+
+点击查看答案
+
+**答案:B**。Gin 本身不内置超时,需在 `http.Server{ReadTimeout: ...}` 配置。
+
+> 来源:`[[GIN/4-context-lifecycle]]` §7
+
+
+
+---
+
+### Q14 【绑定校验】`c.ShouldBind(&req)` 自动检测内容类型的顺序是?
+
+A. form data → query string → JSON
+B. query string → form data → JSON
+C. JSON → form data → query string
+D. 根据 Content-Type 头直接判定,不按顺序
+
+点击查看答案
+
+**答案:C**。先试 JSON(检查 Content-Type),再试 form,最后试 query。
+
+> 来源:`[[GIN/5-binding-validation]]` §1
+
+
+
+---
+
+### Q15 【未知字段】`ShouldBindJSON` 对 JSON body 中的未知字段(struct 中没有对应 key)的默认行为是?
+
+A. 返回 error,绑定失败
+B. 静默忽略,值为零值
+C. 抛出 panic
+D. 打印警告日志但仍继续
+
+点击查看答案
+
+**答案:B**。底层用 `encoding/json.Unmarshal`,对多余字段静默丢弃。
+
+> 来源:`[[GIN/unknown-fields]]` §1
+
+
+
+---
+
+## 二、填空题(每题 5 分,共 40 分)
+
+### Q16 【路由优先级】Gin 路由匹配的优先级从高到低依次是:________ > ________ > ________。
+
+点击查看答案
+
+**答案:静态字符串 > 动态参数 (`:param`) > 通配符 (`*rest`)**
+
+> 来源:`[[GIN/2-routing]]` §4
+
+
+
+---
+
+### Q17 【Engine 结构】`*gin.Engine` 嵌入了 `RouterGroup`,这意味着 Engine 本身就是一颗最大的 RouterGroup,可以直接调用 `.GET()`、`.Use()` 等方法。Engine 中还包含一个 `trees` 字段,其类型是 `methodTrees`(本质是 `[*tree]`),它的作用是:_________________________。
+
+点击查看答案
+
+**答案:每种 HTTP 方法维护一棵独立的 Radix Tree(例如 GET 一棵、POST 一棵)**
+
+> 来源:`[[GIN/1-gin-architecture]]` §1
+
+
+
+---
+
+### Q18 【中间件作用域】Gin 中间件的三级作用域分别是:________、________、________。执行顺序为:________ → ________ → ________ → handler。
+
+点击查看答案
+
+**答案:全局(Engine 级) / 分组(RouterGroup 级) / 路由(单条路由);全局 → 分组 → 路由 → handler**
+
+> 来源:`[[GIN/3-middleware]]` §2
+
+
+
+---
+
+### Q19 【中间件设计模式】Go 标准库 `net/http` 中间件使用 ________ 模式,而 Gin 中间件使用 ________ 模式。
+
+点击查看答案
+
+**答案:装饰器(Decorator)/ 责任链(Chain of Responsibility)**
+
+> 来源:`[[GIN/gin-vs-std]]` §1
+
+
+
+---
+
+### Q20 【c.Abort() 系列方法】Gin 提供了三种 Abort 相关方法:`c.Abort()`、`c.AbortWithStatus(code)`、`_______________`(Abort 同时写入 JSON 响应体)。
+
+点击查看答案
+
+**答案:`c.AbortWithStatusJSON(code, json)`**
+
+> 来源:`[[GIN/middleware-abort]]` §4
+
+
+
+---
+
+### Q21 【c.Copy() 限制】在通过 `c.Copy()` 创建的 goroutine 副本中,________(能/不能)调用 `c.JSON()` 写入响应,但可以读取 `c.Request` 和 `c.Keys`。
+
+点击查看答案
+
+**答案:不能**。`c.Writer` 无法复制,Copy 出的 goroutine 只能读不能写。
+
+> 来源:`[[GIN/3-middleware]]` §5
+
+
+
+---
+
+### Q22 【绑定方法速记】Gin 提供了多种绑定方法:`c.ShouldBindJSON` 绑定 JSON body,`_______________` 绑定 URL 查询参数,`c.ShouldBindUri` 绑定 URI 路径参数,`c.ShouldBindHeader` 绑定 HTTP 请求头。
+
+点击查看答案
+
+**答案:`c.ShouldBindQuery`**
+
+> 来源:`[[GIN/5-binding-validation]]` §1
+
+
+
+---
+
+### Q23 【ShouldBindBodyWith】标准库的 `io.ReadCloser` 类型的 body 只能读取一次。当需要在同一个 handler 中对不同结构体多次解析 body 时,应使用 `_______________` 来缓存 body,避免二次读取报错。
+
+点击查看答案
+
+**答案:`c.ShouldBindBodyWith(&obj, binding.JSON)`**
+
+> 来源:`[[GIN/5-binding-validation]]` §9
+
+
+
+---
+
+## 三、代码补全题(每题约 6 分,共 ~42 分)
+
+### Q24 【CORS 中间件】补全以下 CORS 中间件,处理 OPTIONS 预检请求:
+
+```go
+func cors() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ origin := c.Request.Header.Get("Origin")
+ if origin != "" {
+ c.Header("Access-Control-Allow-Origin", origin)
+ c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS")
+ c.Header("Access-Control-Allow-Headers", "Origin,Content-Type,Authorization")
+ }
+
+ // 处理 OPTIONS 预检请求
+ if c.Request.Method == "OPTIONS" {
+ c.AbortWithStatus(http.StatusNoContent)
+ _______ // ← 补全此行:立即返回,不再执行后续逻辑
+ }
+
+ c.Next() // 非 OPTIONS 请求,继续执行
+ }
+}
+```
+
+点击查看答案
+
+**答案:`return`**
+
+CORS 中间件中 OPTIONS 预检请求处理后必须 `return`,否则会继续执行 `c.Next()` 并可能触发下游 handler。
+
+> 来源:`[[GIN/3-middleware]]` §4
+
+
+
+---
+
+### Q25 【JWT 认证中间件】补全 JWT 认证中间件中的认证失败处理和用户信息存储:
+
+```go
+func jwtAuth() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ token := c.GetHeader("Authorization")
+ if token == "" {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
+ c.Abort()
+ _______ // ← 补全:阻止代码继续向下执行
+ }
+
+ claims, err := parseJWT(token)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
+ c.Abort()
+ _______ // ← 补全:同上
+ }
+
+ // 认证成功,将用户信息存入 Context
+ c.Set("userID", claims.UserID)
+ c.Set("role", claims.Role)
+ _______ // ← 补全:将控制权交给后续中间件/handler
+ }
+}
+```
+
+点击查看答案
+
+**答案:两行 `return`,最后一行 `c.Next()`**
+
+核心规则:`c.Abort()` 之后必须紧跟 `return`;认证成功后调用 `c.Next()` 推进链。
+
+> 来源:`[[GIN/3-middleware]]` §4
+
+
+
+---
+
+### Q26 【异步 Goroutine 安全修复】下面的代码有线程安全问题,请修复:
+
+```go
+func asyncProcessor() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Next()
+
+ // ❌ 危险:c 可能在 goroutine 运行时被回收并复用于其他请求
+ go func() {
+ log.Printf("处理完成: %s, 用户: %s", c.Request.URL.Path, c.GetString("userID"))
+ }()
+ }
+}
+```
+
+**修正:**
+
+```go
+func safeAsyncProcessor() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Next()
+
+ // ✅ 修复:用 c.Copy() 创建独立副本
+ copy := c.Copy()
+ go func() {
+ log.Printf("处理完成: %s, 用户: %s",
+ copy.Request.URL.Path,
+ copy.GetString("userID")) // ← 补全:使用副本而不是 c
+ }()
+ }
+}
+```
+
+查看要点
+
+关键改动:① `c.Copy()` 创建副本;② goroutine 中使用 `copy` 而非 `c`。
+
+> 来源:`[[GIN/3-middleware]]` §5
+
+
+
+---
+
+### Q27 【Context 生命周期 —— reset 清空项】补全 `reset` 方法中被清空的字段名(至少写出 4 个):
+
+```go
+func (c *Context) reset(w http.ResponseWriter) {
+ c.Writer = w.(*responseWriter)
+ c.writerMem.Reset()
+ c.Params = c.Params[:0] // ← 清空路径参数
+ c.handlers = nil // ← 清空 handler 链
+ c.index = -1 // ← 重置执行位置
+ c.errors = c.errors[:0] // ← 清空错误列表
+ c.Keys = nil // ← 清空共享数据
+ c.QueryCache = nil // ← 清空查询缓存
+ c.FormCache = nil // ← 清空表单缓存
+}
+```
+
+请从上方列出你记得住的所有被清空字段:
+
+1. `c.Params = ______________`
+2. `c.handlers = ______________`
+3. `c.index = ______________`
+4. `c.errors = ______________`
+5. `c.Keys = ______________`
+
+点击查看答案
+
+1. `c.Params[:0]`
+2. `nil`
+3. `-1`
+4. `c.errors[:0]`
+5. `nil`
+
+> 来源:`[[GIN/4-context-lifecycle]]` §3
+
+
+
+---
+
+### Q28 【自定义 Validator】补全自定义校验器 `username` 的实现:用户名需满足字母数字下划线组合,长度 3~20 字符。
+
+```go
+func init() {
+ validator.Validator.RegisterValidation("username", func(v validator.FieldLevel) bool {
+ name := v.Field().String()
+ if len(name) < 3 || len(name) > 20 {
+ return false
+ }
+ return regexp.MustCompile(`_______________`).MatchString(name)
+ })
+}
+```
+
+点击查看答案
+
+**答案:`^[a-zA-Z0-9_]+$`**
+
+完整正则确保只允许字母、数字和下划线。
+
+> 来源:`[[GIN/5-binding-validation]]` §6
+
+
+
+---
+
+### Q29 【HTTP 启动方式】补全三种 Gin 启动方式的等价代码:
+
+```go
+r := gin.Default()
+
+// 方式一:框架封装(最常用)
+r.Run(":8080")
+
+// 方式二:标准库直接启动(完全等价)
+http.ListenAndServe(":8080", _______)
+
+// 方式三:高级控制(推荐生产环境)
+srv := &http.Server{
+ Addr: ":8080",
+ Handler: _______, // 传入 Gin Engine
+ ReadTimeout: 5 * time.Second,
+ WriteTimeout: 10 * time.Second,
+}
+_______ // ← 补全第三行的启动调用
+```
+
+点击查看答案
+
+三个空依次为:`r`、`r`、`srv.ListenAndServe()`
+
+> 来源:`[[GIN/engine-handler]]` §2
+
+
+
+---
+
+### Q30 【严格 JSON 模式】补全使用 `json.Decoder.DisallowUnknownFields()` 实现严格模式的方法:
+
+```go
+func strictJSONHandler(c *gin.Context) {
+ var req CreateUserRequest
+
+ decoder := json.NewDecoder(c.Request.Body)
+ decoder._______________ // ← 禁止未知字段
+ if err := decoder.Decode(&req); err != nil {
+ c.JSON(400, gin.H{
+ "code": 1003,
+ "message": "参数解析失败",
+ "error": err.Error(),
+ })
+ _______ // ← 补全:停止处理
+ }
+ // req 已包含所有已知字段,且无拼写错误
+ c.JSON(201, gin.H{"message": "ok"})
+}
+```
+
+点击查看答案
+
+两个空依次为:`DisallowUnknownFields()`、`return`
+
+> 来源:`[[GIN/unknown-fields]]` §3
+
+
+
+---
+
+## 四、综合场景题(附加挑战,可选)
+
+### Q31 【场景题】一个线上服务出现偶发的 "user not found" 错误。排查发现某个 handler 中有一段类似这样的代码:
+
+```go
+var savedUserID string
+
+func myHandler(c *gin.Context) {
+ savedUserID = c.GetString("user_id") // 保存到一个全局变量
+ c.JSON(200, gin.H{"ok": true})
+}
+
+func backgroundWorker() {
+ _ = savedUserID // 在其他地方读取这个全局变量
+}
+```
+
+请问这段代码可能引发什么问题?应该如何修复?
+
+点击查看答案
+
+**问题:** `savedUserID = c.GetString("user_id")` 虽然是拷贝值,但如果改为 `globalC = c`(保存 Context 引用),则会导致数据错乱——因为 Context 被 sync.Pool 复用,另一个请求的 reset() 会清空该对象的 Keys。即使拷贝值,在全局变量中也会有并发写的竞态。
+
+**修复方案:**
+1. 不要使用全局变量保存请求相关数据
+2. 如果确实需要异步使用数据,用 `c.Copy()` 创建副本,或在 goroutine 中只传基本类型值
+3. 参考:`[[GIN/context-pool]]` §2 和 `[[GIN/context-pool-safety]]`
+
+
+
+---
+
+### Q32 【场景题】你的 API 有以下路由注册顺序:
+
+```go
+r := gin.Default()
+
+r.GET("/users/:id", getUser) // 先注册动态参数
+r.GET("/users/list", listAll) // 后注册静态路由
+```
+
+当客户端请求 `GET /users/list` 时,会命中哪个 handler?为什么?注册顺序会影响结果吗?
+
+点击查看答案
+
+会命中 `listAll`(`/users/list`)。**注册顺序不影响匹配结果**。Gin 的 Radix Tree 按优先级决定匹配:静态路由优先级高于动态参数,无论谁先注册,`/users/list` 都是精确匹配静态字符串,必优于 `/users/:id` 的动态参数匹配。
+
+> 来源:`[[GIN/2-routing]]` §4 — "注意:如果有两条同类型的路由,Gin 注册时会 panic——不允许重复。" 不同类型的优先级由 Radix Tree 结构保证,与注册顺序无关。
+
+
+
+---
+
+## 五、速查表
+
+### 中间件执行链路速记
+
+```
+请求进来 → Logger(前置) → Recovery(前置) → Auth(前置) → Handler →
+Auth(后置) → Recovery(后置) → Logger(后置) → 归还 Context 到 pool
+```
+
+### ShouldBind 全家桶速记
+
+| 数据源 | 方法 |
+|--------|------|
+| JSON body | `c.ShouldBindJSON(&v)` |
+| Query string | `c.ShouldBindQuery(&v)` |
+| 自动检测 | `c.ShouldBind(&v)` |
+| URI 路径参数 | `c.ShouldBindUri(&v)` |
+| HTTP 请求头 | `c.ShouldBindHeader(&v)` |
+| Form + JSON 兼容 | `c.ShouldBindBodyWith(&v, binding.Form)` |
+
+### binding 标签速记
+
+`required` `email` `url` `min=N` `max=N` `numeric` `alphanum` `oneof=X Y Z`
+
+---
+
+*本题库基于 `[[GIN/README]]` 索引下的 Level 1→5 共 16 篇笔记整理。*