From 8b0779899154b7da8aa25dc457c640dde5cc32d6 Mon Sep 17 00:00:00 2001 From: wonder Date: Tue, 28 Apr 2026 08:53:28 +0800 Subject: [PATCH] vault backup: 2026-04-28 08:53:28 --- BACKEND/GIN/10-template-rendering.md | 239 ++++++ BACKEND/GIN/11-static-files.md | 194 +++++ BACKEND/GIN/12-server-config.md | 193 +++++ BACKEND/GIN/13-graceful-shutdown.md | 208 +++++ BACKEND/GIN/14-logging.md | 242 ++++++ BACKEND/GIN/15-advanced-running.md | 184 +++++ BACKEND/GIN/3-middleware.md | 8 +- .../3-middleware/cors-registration-scope.md | 14 +- .../cors-registration-scope/cors-preflight.md | 184 +++++ BACKEND/GIN/3-middleware/jwt-auth-qa.md | 2 +- .../middleware-abort.md} | 0 BACKEND/GIN/4-context-lifecycle.md | 4 +- .../context-pool-safety.md | 109 +++ BACKEND/GIN/5-binding-validation.md | 4 +- .../5-binding-validation/unknown-fields.md | 160 ++++ BACKEND/GIN/6-error-handling.md | 258 +++++++ BACKEND/GIN/7-binding-advanced.md | 225 ++++++ BACKEND/GIN/8-file-upload.md | 214 ++++++ BACKEND/GIN/9-response-rendering.md | 185 +++++ BACKEND/GIN/README.md | 44 +- CLAUDE.md | 2 +- GIN/3-middleware/cors-preflight.md | 0 TEST/1-gin-review-quiz.md | 715 ++++++++++++++++++ 23 files changed, 3349 insertions(+), 39 deletions(-) create mode 100644 BACKEND/GIN/10-template-rendering.md create mode 100644 BACKEND/GIN/11-static-files.md create mode 100644 BACKEND/GIN/12-server-config.md create mode 100644 BACKEND/GIN/13-graceful-shutdown.md create mode 100644 BACKEND/GIN/14-logging.md create mode 100644 BACKEND/GIN/15-advanced-running.md create mode 100644 BACKEND/GIN/3-middleware/cors-registration-scope/cors-preflight.md rename BACKEND/GIN/{3-middleware-abort.md => 3-middleware/middleware-abort.md} (100%) create mode 100644 BACKEND/GIN/4-context-lifecycle/context-pool-safety.md create mode 100644 BACKEND/GIN/5-binding-validation/unknown-fields.md create mode 100644 BACKEND/GIN/6-error-handling.md create mode 100644 BACKEND/GIN/7-binding-advanced.md create mode 100644 BACKEND/GIN/8-file-upload.md create mode 100644 BACKEND/GIN/9-response-rendering.md create mode 100644 GIN/3-middleware/cors-preflight.md create mode 100644 TEST/1-gin-review-quiz.md 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 篇笔记整理。*