--- 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 篇笔记整理。*