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

20 KiB
Raw Blame History

tags, create time
tags create time
后端
Go
Gin
复习
测试
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 【路由分组】以下代码输出的完整路径是什么?

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。完整的输出顺序是?

// 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 预检请求:

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 认证中间件中的认证失败处理和用户信息存储:

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 安全修复】下面的代码有线程安全问题,请修复:

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"))
        }()
    }
}

修正:

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 个):

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 字符。

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 启动方式的等价代码:

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() 实现严格模式的方法:

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 中有一段类似这样的代码:

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 有以下路由注册顺序:

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