vault backup: 2026-04-28 08:53:28
This commit is contained in:
@@ -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
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。`gin.Default()` = `gin.New()` + `Use(Logger())` + `Use(Recovery())`。
|
||||
|
||||
> 来源:`[[GIN/1-gin-architecture]]` §4
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q2 【架构/Context】Gin 的 `*gin.Engine` 实现了标准库中哪个接口,从而可以无缝传给 `http.ListenAndServe`?
|
||||
|
||||
A. `http.RoundTripper`
|
||||
B. `http.Handler`
|
||||
C. `http.ResponseWriter`
|
||||
D. `http.ServeMux`
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。`ServeHTTP(ResponseWriter, *Request)` 签名匹配。
|
||||
|
||||
> 来源:`[[GIN/1-gin-architecture/engine-handler]]` §1
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q3 【路由】Gin 的路由匹配算法使用什么数据结构?
|
||||
|
||||
A. `map[string]Handler`
|
||||
B. Radix Tree(基数树/压缩前缀树)
|
||||
C. AVL Tree(平衡二叉树)
|
||||
D. Trie(朴素前缀树,未压缩)
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。Radix Tree 通过前缀共享实现 O(d) 匹配,d 为 URL 深度。
|
||||
|
||||
> 来源:`[[GIN/2-routing]]` §3;`[[GIN/2-routing-complexity-comparison]]`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q4 【路由优先级】当同时注册 `/users/list`(静态)、`/users/:id`(动态)、`/users/*path`(通配符)时,请求 `/users/5` 会命中哪个路由?
|
||||
|
||||
A. `/users/list` — 因为它是第一个注册的
|
||||
B. `/users/:id` — 静态优先于动态
|
||||
C. `/users/*path` — 通配符是兜底规则
|
||||
D. 取决于注册顺序
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。优先级:静态 > 动态参数 > 通配符,与注册顺序无关。
|
||||
|
||||
> 来源:`[[GIN/2-routing]]` §4
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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` — 只取最后一段
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:C**。`basePath` 逐层累加:`"" → "/api" → "/api/v1" → "/api/v1/users/:id"`。
|
||||
|
||||
> 来源:`[[GIN/2-routing]]` §5
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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 步
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。Gin 复杂度 O(d),与路由总数 R 无关;ServeMux O(R×L)。
|
||||
|
||||
> 来源:`[[GIN/2-routing-complexity-comparison]]` §3
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:A**。前置按注册顺序,后置按逆序——栈式行为。
|
||||
|
||||
> 来源:`[[GIN/3-middleware]]` §2
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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`
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:C**。这是装饰器模式,层层嵌套包装。
|
||||
|
||||
> 来源:`[[GIN/gin-vs-std]]` §1
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q9 【c.Abort()】中间件 A 中调用了 `c.Abort()`(紧接 `return`),以下说法正确的是?
|
||||
|
||||
A. 后续中间件和 handler 都跳过,但 A 的后置逻辑会执行
|
||||
B. 后续中间件跳过,handler 执行
|
||||
C. 整个链(包括后续中间件、handler、A 的所有剩余代码)都不再执行
|
||||
D. 只有同一路由的中间件被跳过,其他路由不受影响
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:C**。`c.Abort()` 将 index 设为链长度,`c.Next()` 循环条件立即不满足。Abort 后必须紧跟 return。
|
||||
|
||||
> 来源:`[[GIN/middleware-abort]]`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q10 【中间件 Goroutine】在中间件中启动 goroutine 异步处理日志,正确的做法是?
|
||||
|
||||
A. 直接在 goroutine 中使用 `c`
|
||||
B. 先用 `c.Copy()` 创建副本,在 goroutine 中使用副本
|
||||
C. 把 `c` 保存到全局变量,在 goroutine 中读取
|
||||
D. 使用 `context.WithCancel` 取消原 context
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。`c.Copy()` 创建独立副本,goroutine 中只能读不能写响应。
|
||||
|
||||
> 来源:`[[GIN/3-middleware]]` §5
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q11 【Context 池化】如果在 handler 中把 `*gin.Context` 保存到全局变量,下次请求时会发生什么?
|
||||
|
||||
A. 读到的是上一次请求的数据,完全安全
|
||||
B. 可能读到任意并发请求正在使用的数据,造成数据错乱
|
||||
C. Go 运行时会 panic,因为存在数据竞争
|
||||
D. Context 会自动深拷贝,所以没问题
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。sync.Pool 复用对象,字段被 reset 覆盖,全局引用指向的是被新请求改写后的同一个内存地址。
|
||||
|
||||
> 来源:`[[GIN/context-pool]]` §2
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q12 【Context reset】Context 从 pool 取出后,`reset()` 方法把 `index` 重置为多少?为什么?
|
||||
|
||||
A. `0` — 表示从头开始
|
||||
B. `-1` — 表示还未开始执行,第一次 `c.Next()` 走到 index+1=0
|
||||
C. `-1` — 表示无效值,需要用 -1 做判断
|
||||
D. `nil` — 空表示未初始化
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。`-1` 表示还未开始,`c.Next()` 先 `index++` 到 0,再执行 `handlers[0]`。
|
||||
|
||||
> 来源:`[[GIN/4-context-lifecycle]]` §3
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q13 【超时控制】Gin 默认是否有 HTTP 请求超时控制?如果需要超时,应该在哪里配置?
|
||||
|
||||
A. 有,默认 30 秒超时
|
||||
B. 没有,应在 `http.Server` 层配置 `ReadTimeout`/`WriteTimeout`
|
||||
C. 没有,但可用 `c.WithTimeout()` 设置
|
||||
D. 有,在 `gin.Default()` 内部已经设置了
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。Gin 本身不内置超时,需在 `http.Server{ReadTimeout: ...}` 配置。
|
||||
|
||||
> 来源:`[[GIN/4-context-lifecycle]]` §7
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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 头直接判定,不按顺序
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:C**。先试 JSON(检查 Content-Type),再试 form,最后试 query。
|
||||
|
||||
> 来源:`[[GIN/5-binding-validation]]` §1
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q15 【未知字段】`ShouldBindJSON` 对 JSON body 中的未知字段(struct 中没有对应 key)的默认行为是?
|
||||
|
||||
A. 返回 error,绑定失败
|
||||
B. 静默忽略,值为零值
|
||||
C. 抛出 panic
|
||||
D. 打印警告日志但仍继续
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:B**。底层用 `encoding/json.Unmarshal`,对多余字段静默丢弃。
|
||||
|
||||
> 来源:`[[GIN/unknown-fields]]` §1
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 二、填空题(每题 5 分,共 40 分)
|
||||
|
||||
### Q16 【路由优先级】Gin 路由匹配的优先级从高到低依次是:________ > ________ > ________。
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:静态字符串 > 动态参数 (`:param`) > 通配符 (`*rest`)**
|
||||
|
||||
> 来源:`[[GIN/2-routing]]` §4
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q17 【Engine 结构】`*gin.Engine` 嵌入了 `RouterGroup`,这意味着 Engine 本身就是一颗最大的 RouterGroup,可以直接调用 `.GET()`、`.Use()` 等方法。Engine 中还包含一个 `trees` 字段,其类型是 `methodTrees`(本质是 `[*tree]`),它的作用是:_________________________。
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:每种 HTTP 方法维护一棵独立的 Radix Tree(例如 GET 一棵、POST 一棵)**
|
||||
|
||||
> 来源:`[[GIN/1-gin-architecture]]` §1
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q18 【中间件作用域】Gin 中间件的三级作用域分别是:________、________、________。执行顺序为:________ → ________ → ________ → handler。
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:全局(Engine 级) / 分组(RouterGroup 级) / 路由(单条路由);全局 → 分组 → 路由 → handler**
|
||||
|
||||
> 来源:`[[GIN/3-middleware]]` §2
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q19 【中间件设计模式】Go 标准库 `net/http` 中间件使用 ________ 模式,而 Gin 中间件使用 ________ 模式。
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:装饰器(Decorator)/ 责任链(Chain of Responsibility)**
|
||||
|
||||
> 来源:`[[GIN/gin-vs-std]]` §1
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q20 【c.Abort() 系列方法】Gin 提供了三种 Abort 相关方法:`c.Abort()`、`c.AbortWithStatus(code)`、`_______________`(Abort 同时写入 JSON 响应体)。
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:`c.AbortWithStatusJSON(code, json)`**
|
||||
|
||||
> 来源:`[[GIN/middleware-abort]]` §4
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q21 【c.Copy() 限制】在通过 `c.Copy()` 创建的 goroutine 副本中,________(能/不能)调用 `c.JSON()` 写入响应,但可以读取 `c.Request` 和 `c.Keys`。
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:不能**。`c.Writer` 无法复制,Copy 出的 goroutine 只能读不能写。
|
||||
|
||||
> 来源:`[[GIN/3-middleware]]` §5
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q22 【绑定方法速记】Gin 提供了多种绑定方法:`c.ShouldBindJSON` 绑定 JSON body,`_______________` 绑定 URL 查询参数,`c.ShouldBindUri` 绑定 URI 路径参数,`c.ShouldBindHeader` 绑定 HTTP 请求头。
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:`c.ShouldBindQuery`**
|
||||
|
||||
> 来源:`[[GIN/5-binding-validation]]` §1
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q23 【ShouldBindBodyWith】标准库的 `io.ReadCloser` 类型的 body 只能读取一次。当需要在同一个 handler 中对不同结构体多次解析 body 时,应使用 `_______________` 来缓存 body,避免二次读取报错。
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:`c.ShouldBindBodyWith(&obj, binding.JSON)`**
|
||||
|
||||
> 来源:`[[GIN/5-binding-validation]]` §9
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 三、代码补全题(每题约 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 请求,继续执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:`return`**
|
||||
|
||||
CORS 中间件中 OPTIONS 预检请求处理后必须 `return`,否则会继续执行 `c.Next()` 并可能触发下游 handler。
|
||||
|
||||
> 来源:`[[GIN/3-middleware]]` §4
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:两行 `return`,最后一行 `c.Next()`**
|
||||
|
||||
核心规则:`c.Abort()` 之后必须紧跟 `return`;认证成功后调用 `c.Next()` 推进链。
|
||||
|
||||
> 来源:`[[GIN/3-middleware]]` §4
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
}()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details><summary>查看要点</summary>
|
||||
|
||||
关键改动:① `c.Copy()` 创建副本;② goroutine 中使用 `copy` 而非 `c`。
|
||||
|
||||
> 来源:`[[GIN/3-middleware]]` §5
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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 = ______________`
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
1. `c.Params[:0]`
|
||||
2. `nil`
|
||||
3. `-1`
|
||||
4. `c.errors[:0]`
|
||||
5. `nil`
|
||||
|
||||
> 来源:`[[GIN/4-context-lifecycle]]` §3
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**答案:`^[a-zA-Z0-9_]+$`**
|
||||
|
||||
完整正则确保只允许字母、数字和下划线。
|
||||
|
||||
> 来源:`[[GIN/5-binding-validation]]` §6
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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,
|
||||
}
|
||||
_______ // ← 补全第三行的启动调用
|
||||
```
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
三个空依次为:`r`、`r`、`srv.ListenAndServe()`
|
||||
|
||||
> 来源:`[[GIN/engine-handler]]` §2
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 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"})
|
||||
}
|
||||
```
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
两个空依次为:`DisallowUnknownFields()`、`return`
|
||||
|
||||
> 来源:`[[GIN/unknown-fields]]` §3
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 四、综合场景题(附加挑战,可选)
|
||||
|
||||
### 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 // 在其他地方读取这个全局变量
|
||||
}
|
||||
```
|
||||
|
||||
请问这段代码可能引发什么问题?应该如何修复?
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
**问题:** `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]]`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Q32 【场景题】你的 API 有以下路由注册顺序:
|
||||
|
||||
```go
|
||||
r := gin.Default()
|
||||
|
||||
r.GET("/users/:id", getUser) // 先注册动态参数
|
||||
r.GET("/users/list", listAll) // 后注册静态路由
|
||||
```
|
||||
|
||||
当客户端请求 `GET /users/list` 时,会命中哪个 handler?为什么?注册顺序会影响结果吗?
|
||||
|
||||
<details><summary>点击查看答案</summary>
|
||||
|
||||
会命中 `listAll`(`/users/list`)。**注册顺序不影响匹配结果**。Gin 的 Radix Tree 按优先级决定匹配:静态路由优先级高于动态参数,无论谁先注册,`/users/list` 都是精确匹配静态字符串,必优于 `/users/:id` 的动态参数匹配。
|
||||
|
||||
> 来源:`[[GIN/2-routing]]` §4 — "注意:如果有两条同类型的路由,Gin 注册时会 panic——不允许重复。" 不同类型的优先级由 Radix Tree 结构保证,与注册顺序无关。
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 五、速查表
|
||||
|
||||
### 中间件执行链路速记
|
||||
|
||||
```
|
||||
请求进来 → 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 篇笔记整理。*
|
||||
Reference in New Issue
Block a user