应使用 context.withtimeout 在中间件中为单个请求设置超时,通过 c.request.withcontext(ctx) 注入,并在 c.next() 后检查 ctx.err() 调用 c.abort();超时值宜从配置中心读取,子 goroutine 需传递该 ctx。

怎么用 context.WithTimeout 给单个请求设超时
Gin 本身不自动中断慢请求,必须靠 context.WithTimeout 显式注入超时控制。不加这层,哪怕 handler 卡死 10 分钟,连接也一直挂着,拖垮整个服务。
关键点是:超时必须在中间件里设,且 c.Next() 后要检查 ctx.Err() 并调用 c.Abort(),否则后续 handler 还会执行。
-
c.Request = c.Request.WithContext(ctx)这一步不能漏,否则业务 handler 拿不到新 context - 超时时间别硬写死,建议从配置中心读取,比如
5 * time.Second适合普通 API,文件上传类接口得调到 30s+ - 如果 handler 内启了子 goroutine(比如异步发消息),必须把
ctx传进去,并用select { case 监听取消
为什么只靠中间件还不够:业务代码里还得自己 check ctx
中间件里的 context.WithTimeout 只负责兜底返回 504 和终止流程,它不会自动中断正在运行的业务逻辑。比如数据库查询、HTTP 外部调用、循环处理大数组——这些都得你自己在代码里判断 ctx.Err() == context.DeadlineExceeded 并提前退出。
常见错误是以为加了超时中间件就万事大吉,结果 DB 查询卡住 20 秒,中间件早返回了 504,但 goroutine 还在后台跑着,连带占着连接池和内存。
- 所有阻塞操作前都要加
select判断:select { case - 用
database/sql时,传入ctx而不是context.Background(),比如db.QueryRowContext(ctx, ...) - 调外部 HTTP 接口必须用
http.DefaultClient.Do(req.WithContext(ctx)),否则超时对它无效
Redis 或 DB 调用卡住时,超时中间件为何没生效
因为中间件的 context.WithTimeout 只影响当前请求的生命周期,而 Redis/DB 客户端默认不感知这个 context——除非你显式把 context 传进去。很多老项目还在用 redis.Get(...) 这种无 context 版本,超时完全不起作用。
典型现象:API 响应时间监控显示平均 200ms,但 P99 突然飙到 15s,日志里全是 context deadline exceeded,说明超时已触发,但下游调用没响应。
- Redis 客户端必须用支持 context 的方法,如
client.Get(ctx, key).Val()(go-redis/v8) - PostgreSQL 用
pgx.Conn.QueryRow(ctx, ...),MySQL 用db.QueryRowContext(ctx, ...) - 自定义封装的工具函数(比如通用缓存 get/set)也要把
ctx作为第一个参数,禁止内部 new context
并发启动多个 goroutine 时,超时传播容易被忽略
一个 handler 启 3 个 goroutine 并行查数据,主 goroutine 收到 ctx.Done() 后直接 return,但另外 2 个可能还在跑。它们没监听 cancel,也不释放资源,形成 goroutine 泄漏。
这不是 Gin 的问题,是 Go 并发模型的基本约束:context 取消不会自动 kill goroutine,只能靠协作式退出。
- 每个子 goroutine 开头就要
select { case - 用
errgroup.Group替代裸go,它能自动传播 cancel 并等待全部完成或失败 - 避免在 goroutine 里直接用
c(*gin.Context),它不是线程安全的;需要的数据应提前拷贝或传参
真正难的不是加超时中间件,而是让每一层调用链都尊重 context —— 从路由中间件,到业务 handler,再到 DB/Redis/HTTP 客户端,最后到你写的每个工具函数。漏掉任意一环,超时就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











