7.3 KiB
id, title, type, domain, stack, tags, level, related, week
| id | title | type | domain | stack | tags | level | related | week | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.gin.interview | Gin 面试题 | interview | backend |
|
|
intermediate |
|
|
Gin 面试题
在本计划里的体现:
todo-api(第 1~2 周)、task-board(第 3 周)。
基础
【基础】Gin 和标准库 net/http 是什么关系?
参考答案:Gin 的 Engine 实现了 http.Handler 接口,底层仍然由 net/http 负责监听和连接管理。Gin 增加了基于前缀树的高性能路由、gin.Context(封装请求和响应、参数获取、绑定、中间件间传值)、中间件链和常用工具(JSON 渲染、校验)。所以 Gin 可以和任何标准库中间件、httptest 一起使用。
追问:Gin 的 *gin.Context 和标准库的 context.Context 是什么关系?→ gin.Context 是 Gin 自己的请求上下文(参数、响应、中间件链),它也实现了 context.Context 接口;向下游传递取消信号时,用 c.Request.Context() 更明确。
【基础】Gin 怎么获取路径参数、查询参数和请求体?
参考答案:路径参数 c.Param("id");查询参数 c.Query("k") / c.DefaultQuery("k", "默认");请求体用绑定 c.ShouldBindJSON(&req),查询参数也可以 c.ShouldBindQuery(&q) 绑定到结构体。绑定时根据结构体的 binding 标签做校验。
追问:ShouldBind 和 Bind 的区别?→ Bind* 失败时自动返回 400 并中止,ShouldBind* 只返回错误、由你决定如何响应,一般统一用后者。
【基础】中间件的执行顺序是怎样的?c.Next() 和 c.Abort() 做什么?
参考答案:按注册顺序依次执行,形成洋葱模型:c.Next() 之前的代码按顺序执行,之后的代码按相反顺序执行。c.Next() 立即执行后续的处理函数;c.Abort() 阻止后续处理函数执行,但当前函数会继续执行完,所以通常 Abort 后紧跟 return。
追问:在中间件里 c.Abort() 之后还需要 return 吗?→ 需要。Abort 只是阻止后续处理函数执行,当前函数后面的代码仍会继续执行。
【基础】RESTful 接口的状态码怎么选?
参考答案:查询成功 200、创建成功 201、删除成功 204、参数错误 400、未认证 401、已认证但无权限 403、不存在 404、状态冲突(如重复创建)409、请求过多 429、服务器错误 500、下游不可用 502/503。
追问:参数校验失败用 400 还是 422?→ 两种都常见,团队统一即可;关键是错误响应格式统一、带上具体的字段错误信息。
进阶
【进阶】JWT 和 Session 有什么区别?JWT 怎么实现退出登录?
参考答案:Session 把登录状态存在服务端(内存或 Redis),客户端只拿一个 Session ID,服务端可以随时让它失效,但需要共享存储。JWT 把用户信息签名后交给客户端,服务端只验签,无状态、易扩展,但签发后在过期前一直有效。退出登录 / 改密码的方案:短有效期访问令牌(如 15 分钟)+ 刷新令牌;服务端维护用户的令牌版本号或黑名单(存 Redis),校验时比对。
追问:令牌存 localStorage 还是 Cookie?→ localStorage 怕 XSS 窃取;httpOnly Cookie 防 XSS 但要防 CSRF(SameSite、CSRF Token)。
【进阶】为什么在 goroutine 里使用 gin.Context 要先 c.Copy()?
参考答案:Gin 用对象池复用 Context,请求处理结束后它会被重置给下一个请求使用。如果后台 goroutine 还持有原来的 c,读到的可能是别的请求的数据。c.Copy() 生成只读副本。更好的做法是只把需要的值(用户 ID、请求 ID、c.Request.Context() 派生的 context)传给后台任务。
追问:goroutine 里需要请求的取消信号怎么办?→ 后台任务通常不应该跟随请求取消(请求结束就被取消),用 context.WithoutCancel(c.Request.Context()) 或新的 context 并设置独立超时。
【进阶】如何设计统一的错误处理?
参考答案:定义业务错误类型(HTTP 状态 + 业务码 + 用户提示),处理函数通过 c.Error(err) 记录错误并返回,由一个错误中间件统一转换成标准响应;未知错误和 panic 统一返回通用提示,详细信息带请求 ID 写日志。详见本技术栈的场景文档。
追问:错误码和 HTTP 状态码是什么关系?→ HTTP 状态码表达大类(客户端错误 / 服务端错误),业务错误码表达具体原因,方便前端展示和排查;不要所有错误都返回 200。
【进阶】Gin 的路由为什么快?
参考答案:Gin 使用 httprouter 风格的前缀树(radix tree),每个 HTTP 方法一棵树,按路径公共前缀压缩存储,匹配时间与路径长度相关,基本不受路由数量影响;同时 Context 用对象池减少内存分配。
追问:路由冲突(例如 /users/:id 和 /users/new)会怎样?→ 新版本的 Gin 已经支持静态路由和参数路由共存,静态路由优先;旧版本会在注册时 panic。以使用的版本为准,写测试验证。
场景题
【场景题】接口被恶意刷,怎么在 Gin 里做限流?
参考答案:单实例用令牌桶(golang.org/x/time/rate),按用户 ID 或 IP 维护一个限流器 map(注意定期清理);多实例部署需要全局限流,用 Redis(固定窗口 INCR+EXPIRE 或滑动窗口 ZSet、Lua 脚本保证原子)。超限返回 429 并带 Retry-After。更外层可以在 Nginx 或网关做 IP 级限流,应用层做用户级、接口级限流。本计划第 3 周项目一实现过。
追问:多实例部署时限流怎么做?→ 单机限流(x/time/rate)只能限制单个实例;全局限流要用 Redis(滑动窗口 / 令牌桶 + Lua 原子执行)或在网关层统一做。
【场景题】一个上传接口偶尔导致服务内存暴涨,怎么处理?
参考答案:限制请求体大小(http.MaxBytesReader 或 Gin 的 MaxMultipartMemory,Nginx 的 client_max_body_size);大文件流式处理而不是一次读入内存;校验文件类型和大小;上传改成前端直传对象存储(预签名 URL),后端只接收回调。
追问:大文件上传怎么做更好?→ 前端直传对象存储(服务端签发预签名 URL),服务端只处理元数据;需要服务端处理时用流式读取,不要整个读进内存。
【场景题】怎么给 Gin 服务写接口测试?
参考答案:把路由组装放在一个函数里(如 New(deps) *gin.Engine),测试时注入内存版依赖;用 httptest.NewRecorder() 和 r.ServeHTTP(w, req) 直接调用,不启动端口;用表格驱动覆盖成功、参数错误、未登录、无权限、不存在、内部错误等情况;数据库相关的用集成测试单独跑。示例见 examples/unified-api。
追问:测试需要数据库怎么办?→ 业务逻辑通过接口依赖存储层,单元测试用假实现;集成测试用真实数据库(测试库或容器),每个测试使用独立的数据并清理。