--- tags: - 后端 - Go - Gin - 测试 - 自我考察 create time: 2026-04-28 update time: 2026-04-28 status: reviewed --- # Gin 框架自测题 ## 使用说明 本试卷覆盖你笔记中的 **核心机制 → 进阶功能 → 工程实践** 全链路知识点。共四部分:选择题、填空题、代码补全、主观题。 **建议用时:** 45 分钟 | **及格线:** 70 / 100 做完后对照下方「参考答案」自评,错的地方回到对应笔记重新理解。 --- ## 一、选择题(每题 3 分,共 30 分) > 每题只有一个正确答案。 ### Q1.【路由匹配优先级】 注册了以下三条路由: ```go r.GET("/users/list", listAll) r.GET("/users/:id", getOne) r.GET("/users/*path", catchAll) ``` 当收到 `GET /users/list` 请求时,哪个 handler 会被执行? A. `catchAll` — 因为通配符匹配范围最广 B. `getOne` — 因为动态参数 :id 能匹配到 "list" C. `listAll` — 静态路由优先级最高 D. 取决于三者的注册顺序 > [!tip]- Q1 答案 > **C — 静态路由优先级最高** > > **解析:** > Gin 的路由匹配基于 Radix Tree,匹配规则是按**路径片段的类型**决定优先级,而非注册顺序: > > 1. **静态片段(literal)** — 如 `list`,精确字符串匹配,优先级最高 > 2. **动态片段(param)** — 如 `:id`,占位符匹配,优先级居中 > 3. **通配片段(catch-all)** — 如 `*path`,贪婪匹配其余所有路径,优先级最低 > > **为什么错选 A/B/D:** > - ❌ A:通配符确实能匹配 `/users/list`,但它在最后才尝试 > - ❌ B:`:id` 也能捕获 "list" 作为参数值,但静态优先于动态 > - ❌ D:这是标准库 `ServeMux` 的行为,Gin 的 Radix Tree 是结构化匹配,与顺序无关 > > 💡 **拓展:** 如果两条完全相同的 path+method 重复注册,Gin 会 panic,因为此时不存在优先级区分的问题——它们是冲突的。 ### Q2.【中间件执行顺序】 ```go r := gin.Default() r.Use(func(c *gin.Context) { fmt.Print("A"); c.Next(); fmt.Print("a") }) r.Use(func(c *gin.Context) { fmt.Print("B"); c.Next(); fmt.Print("b") }) v1 := r.Group("/v1", func(c *gin.Context) { fmt.Print("C"); c.Next(); fmt.Print("c") }) { v1.GET("/test", func(c *gin.Context) { fmt.Print("H") }) } ``` 访问 `GET /v1/test` 的输出顺序是? A. `ABChaCb` B. `ABCHabc` C. `ABCha b` D. `ChBa aB` > [!tip]- Q2 答案 > **B — `ABCHabc`** > > **解析:** > 中间件执行遵循**洋葱模型(Onion Model)**,需要区分两个维度: > > **前置部分(`c.Next()` 之前)—— 正序执行:** > ``` > r.Use(A) → A开始 > r.Use(B) → B开始 > v1路由组(C) → C开始 > handler(H) → H执行 > ``` > 所以前置输出顺序是:**A B C H** > > **后置部分(`c.Next()` 之后)—— 逆序执行:** > ``` > handler 结束 > v1路由组(C) 的 c.Next() 之后 → c输出 > r.Use(B) 的 c.Next() 之后 → b输出 > r.Use(A) 的 c.Next() 之后 → a输出 > ``` > 所以后置输出顺序是:**c b a** > > 合起来就是 **ABCH + abc = ABCHabc** ✅ > > 💡 **记忆口诀:** "进来正序排排站,出去逆序往回走"。这是所有 Web 框架中间件的通用模式(Express/Koa/NestJS 同理)。 ### Q3.【Context 生命周期】 关于 Gin 使用 `sync.Pool` 复用 Context 的说法,**错误**的是: A. 每次请求从池中取出 Context,请求结束后归还池中 B. 可以在 handler 的 goroutine 中安全地持有 Context 引用,在请求返回后继续使用 C. `c.Reset()` 会把 Keys、Params、handlers、index 全部清空 D. index 被重置为 -1 表示还未开始执行,第一次 `c.Next()` 走到索引 0 > [!tip]- Q3 答案 > **B — Context 池化不安全跨请求持有** > > **解析:** > Gin 为了减少 GC 压力,使用 `sync.Pool` 复用 `Context` 实例。每次请求结束后,Context 会被 `c.Reset()` 清空并放回池中。**下一个请求可能取出同一个 Context 对象**。 > > **逐项分析:** > - ✅ A:正确描述。取出来 → 用 → `c.Reset()` → 放回去,循环复用 > - ❌ B:**错误!** goroutine 异步运行,handler 返回后 Context 已 reset 并被其他请求复用,此时读到的数据可能是下一个请求的(安全漏洞 + 数据错乱) > - ✅ C:`c.Reset()` 确实清空 Keys(map)、Params、handlers slice、index 等核心字段 > - ✅ D:`index = -1` 是初始状态,`c.Next()` 先自增为 0,再执行 handlers[0] > > 💡 **关键陷阱:** 如果你在 handler 里 `go func() { time.Sleep(5s); c.Get("uid") }()`,5 秒后读到的很可能不是你的数据! ### Q4.【绑定与校验】 关于 `binding:"required"` 对不同类型字段的处理,以下说法**正确**的是: A. 对 `string` 类型,值为空字符串 `""` 时校验失败 B. 对 `*string` 指针类型,指向 nil 时校验失败 C. 对 `int` 类型,值为 0 时校验失败 D. A 和 B 都正确,C 不正确 > [!tip]- Q4 答案 > **D — required 对 string="" 和 *string=nil 均失败,对 int=0 不失败** > > **解析:** > `validator` 包的 `required` 标签本质上是检查值的 **"零值" (zero value)**,但不同类型的零值判断规则不同: > > | 类型 | 零值 | `required` 是否失败 | > |------|------|---------------------| > | `string` | `""` | ✅ 失败 — 空串意味着前端没传 | > | `*string` (nil) | `nil` | ✅ 失败 — 未分配表示没传 | > | `int` / `int64` | `0` | ❌ **不失败** — 0 是一个合法的业务值 | > | `bool` | `false` | ❌ **不失败** — false 也是合法输入 | > | `[]string` | `nil` 或 `[]` | ✅ 失败 | > > **为什么 C 错?** 如果 0 算"必填",那页面上有个数量选择器默认选 0 就无法提交了。所以 `required` 只对引用类型(指针、slice、map)和 string 生效。 > > 💡 **变通方案:** 如果你需要"int 不能为 0",应该自定义校验器:`binding:"min=1"` 或用自定义 tag。 ### Q5.【c.Copy() 安全性】 在中间件中启动异步 goroutine,下列哪种做法是**安全**的? A. 直接在中间件中 `go func() { c.JSON(200, ...) }()` B. 先 `copy := c.Copy()`,然后 `go func() { copy.GetString("userID") }()` C. 先 `copy := c.Copy()`,然后 `go func() { copy.JSON(200, ...) }()` D. 用 `c.Request.Context().Done()` 判断后再读 c.Keys > [!tip]- Q5 答案 > **B — c.Copy 后 goroutine 只读** > > **解析:** > `c.Copy()` 创建一个轻量级副本,共享 Request 和 Writer(所以不能对 copy 写响应),但拥有独立的 Keys map。 > > **逐项分析:** > - ❌ A:**绝对不行!** goroutine 运行时 handler 已返回,Context 被 reset 放回 pool,此时读写都是数据竞争 > - ✅ B:`c.Copy()` 的 Keys 是独立副本,GetString 只是读取安全 > - ❌ C:copy 的 Writer 和原始 Context **共享**同一个 `http.ResponseWriter`,goroutine 中调用 JSON() 会和主流程竞争写入,导致响应损坏 > - ❌ D:`ctx.Done()` 只能说明请求被取消,不能保证 Context 还没被复用——即使 context 没 done,pool 里的同一个对象也可能被其他请求取走并修改了 Keys > > 💡 **黄金法则:** goroutine 要用的数据,在主流程里先 `取值 → 存局部变量`,而不是依赖 Context。 ### Q6.【SecureJSON 原理】 SecureJSON 防 JSON 劫持的原理是: A. 在 JSON 前添加 `)]}',\n` 前缀,使浏览器无法将其解析为合法的 JavaScript B. 自动设置 `X-Content-Type-Options: nosniff` 响应头 C. 只允许白名单域名通过 CORS 访问 D. 对 JSON body 进行 HMAC 签名验证 > [!tip]- Q6 答案 > **A — SecureJSON 前缀阻断 JS 解析** > > **解析:** > JSON 劫持(JSON Hijacking)的攻击场景:攻击者构造一个恶意页面,用 `