This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/TEST/1-gin-review-quiz.md
T

620 lines
20 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-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
> [!note]- Q1 答案
> **答案: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`
> [!note]- Q2 答案
> **答案: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(朴素前缀树,未压缩)
> [!note]- Q3 答案
> **答案: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. 取决于注册顺序
> [!note]- Q4 答案
> **答案: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` — 只取最后一段
> [!note]- Q5 答案
> **答案: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 步
> [!note]- Q6 答案
> **答案: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
> [!note]- Q7 答案
> **答案: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`
> [!note]- Q8 答案
> **答案:C**。这是装饰器模式,层层嵌套包装。
>
> > 来源:`[[GIN/gin-vs-std]]` §1
---
### Q9 【c.Abort()】中间件 A 中调用了 `c.Abort()`(紧接 `return`),以下说法正确的是?
A. 后续中间件和 handler 都跳过,但 A 的后置逻辑会执行
B. 后续中间件跳过,handler 执行
C. 整个链(包括后续中间件、handler、A 的所有剩余代码)都不再执行
D. 只有同一路由的中间件被跳过,其他路由不受影响
> [!note]- Q9 答案
> **答案: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
> [!note]- Q10 答案
> **答案:B**。`c.Copy()` 创建独立副本,goroutine 中只能读不能写响应。
>
> > 来源:`[[GIN/3-middleware]]` §5
---
### Q11 【Context 池化】如果在 handler 中把 `*gin.Context` 保存到全局变量,下次请求时会发生什么?
A. 读到的是上一次请求的数据,完全安全
B. 可能读到任意并发请求正在使用的数据,造成数据错乱
C. Go 运行时会 panic,因为存在数据竞争
D. Context 会自动深拷贝,所以没问题
> [!note]- Q11 答案
> **答案: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` — 空表示未初始化
> [!note]- Q12 答案
> **答案: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()` 内部已经设置了
> [!note]- Q13 答案
> **答案: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 头直接判定,不按顺序
> [!note]- Q14 答案
> **答案:C**。先试 JSON(检查 Content-Type),再试 form,最后试 query。
>
> > 来源:`[[GIN/5-binding-validation]]` §1
---
### Q15 【未知字段】`ShouldBindJSON` 对 JSON body 中的未知字段(struct 中没有对应 key)的默认行为是?
A. 返回 error,绑定失败
B. 静默忽略,值为零值
C. 抛出 panic
D. 打印警告日志但仍继续
> [!note]- Q15 答案
> **答案:B**。底层用 `encoding/json.Unmarshal`,对多余字段静默丢弃。
>
> > 来源:`[[GIN/unknown-fields]]` §1
---
## 二、填空题(每题 5 分,共 40 分)
### Q16 【路由优先级】Gin 路由匹配的优先级从高到低依次是:________ > ________ > ________。
> [!note]- Q16 答案
> **答案:静态字符串 > 动态参数 (`:param`) > 通配符 (`*rest`)**
>
> > 来源:`[[GIN/2-routing]]` §4
---
### Q17 【Engine 结构】`*gin.Engine` 嵌入了 `RouterGroup`,这意味着 Engine 本身就是一颗最大的 RouterGroup,可以直接调用 `.GET()`、`.Use()` 等方法。Engine 中还包含一个 `trees` 字段,其类型是 `methodTrees`(本质是 `[*tree]`),它的作用是:_________________________。
> [!note]- Q17 答案
> **答案:每种 HTTP 方法维护一棵独立的 Radix Tree(例如 GET 一棵、POST 一棵)**
>
> > 来源:`[[GIN/1-gin-architecture]]` §1
---
### Q18 【中间件作用域】Gin 中间件的三级作用域分别是:________、________、________。执行顺序为:________ → ________ → ________ → handler。
> [!note]- Q18 答案
> **答案:全局(Engine 级) / 分组(RouterGroup 级) / 路由(单条路由);全局 → 分组 → 路由 → handler**
>
> > 来源:`[[GIN/3-middleware]]` §2
---
### Q19 【中间件设计模式】Go 标准库 `net/http` 中间件使用 ________ 模式,而 Gin 中间件使用 ________ 模式。
> [!note]- Q19 答案
> **答案:装饰器(Decorator)/ 责任链(Chain of Responsibility)**
>
> > 来源:`[[GIN/gin-vs-std]]` §1
---
### Q20 【c.Abort() 系列方法】Gin 提供了三种 Abort 相关方法:`c.Abort()`、`c.AbortWithStatus(code)`、`_______________`(Abort 同时写入 JSON 响应体)。
> [!note]- Q20 答案
> **答案:`c.AbortWithStatusJSON(code, json)`**
>
> > 来源:`[[GIN/middleware-abort]]` §4
---
### Q21 【c.Copy() 限制】在通过 `c.Copy()` 创建的 goroutine 副本中,________(能/不能)调用 `c.JSON()` 写入响应,但可以读取 `c.Request` 和 `c.Keys`。
> [!note]- Q21 答案
> **答案:不能**。`c.Writer` 无法复制,Copy 出的 goroutine 只能读不能写。
>
> > 来源:`[[GIN/3-middleware]]` §5
---
### Q22 【绑定方法速记】Gin 提供了多种绑定方法:`c.ShouldBindJSON` 绑定 JSON body,`_______________` 绑定 URL 查询参数,`c.ShouldBindUri` 绑定 URI 路径参数,`c.ShouldBindHeader` 绑定 HTTP 请求头。
> [!note]- Q22 答案
> **答案:`c.ShouldBindQuery`**
>
> > 来源:`[[GIN/5-binding-validation]]` §1
---
### Q23 【ShouldBindBodyWith】标准库的 `io.ReadCloser` 类型的 body 只能读取一次。当需要在同一个 handler 中对不同结构体多次解析 body 时,应使用 `_______________` 来缓存 body,避免二次读取报错。
> [!note]- Q23 答案
> **答案:`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 请求,继续执行
}
}
```
> [!note]- Q24 答案
> **答案:`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
}
}
```
> [!note]- Q25 答案
> **答案:两行 `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
}()
}
}
```
> [!note]- Q26 答案要点
> **关键改动**:① `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 = ______________`
> [!note]- Q27 答案
> 1. `c.Params = c.Params[:0]`
> 2. `nil`
> 3. `-1`
> 4. `c.errors = 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)
})
}
```
> [!note]- Q28 答案
> **答案:`^[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,
}
_______ // ← 补全第三行的启动调用
```
> [!note]- Q29 答案
> **三个空依次为**:`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"})
}
```
> [!note]- Q30 答案
> **两个空依次为**:`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 // 在其他地方读取这个全局变量
}
```
请问这段代码可能引发什么问题?应该如何修复?
> [!note]- Q31 答案
> **问题:** `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?为什么?注册顺序会影响结果吗?
> [!note]- Q32 答案
> 会命中 `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 篇笔记整理。*