必须包裹整个 http 生命周期并注入 db 层或通过 context 透传才能可靠统计 sql 耗时,因仅在 handler 内打点会漏掉 panic 恢复时间、中间件 db 操作及请求路径关联。

为什么不能只在 handler 里统计 SQL 耗时
因为 c.Next() 之后才能拿到完整响应状态,而 SQL 执行可能发生在中间件之后、handler 内部、甚至嵌套调用中;仅在 handler 开始/结束打点,会漏掉 panic 后的 recover 时间、忽略中间件自身 DB 操作、且无法关联请求路径与 SQL 上下文。真正可信赖的统计必须包裹整个 HTTP 生命周期,并把 SQL 监控逻辑注入到 DB 层或通过 context 透传。
如何用中间件捕获请求级 SQL 统计维度
核心是把 SQL 耗时、行数、错误、语句摘要(非完整 SQL)和当前路由绑定到 gin.Context,并在中间件结尾统一上报。不要尝试解析 raw SQL——它不可靠、易被注入污染、且性能差。
- 在中间件开头调用
c.Set("sql_start", time.Now()) - 业务 handler 中执行 DB 操作前,用
c.Get("sql_start")获取基准时间,执行后记录耗时并c.Set("sql_duration", d) - 避免在中间件里直接调用 DB —— 那会让统计耦合太重;应由 handler 或 DAO 层主动上报
- 用
c.GetString("path")+c.Request.Method构建指标 key,比如"GET:/api/users" - 注意:
context.WithValue不适合高频写入,优先用c.Set/c.Get
Gin 中间件与 database/sql 的协同陷阱
常见错误是把 *sql.DB 直接塞进中间件闭包,导致所有请求共用同一连接池配置,掩盖了 per-request 的慢查询问题。更糟的是,有人在中间件里调用 db.QueryRow 并期望它反映当前请求的 SQL 行为——其实它只是全局池的一次借用,跟当前请求无关。
- 正确做法:中间件只做「元数据采集」,SQL 实际执行仍由 handler 控制;DB 层封装一个带 context 的
ExecContext,内部自动记录耗时并写回c - 别复用
time.Time变量:每个请求必须 new 出独立 start time,否则并发下会错乱 - 如果用了 GORM,启用
logger.Default.LogMode(logger.SlowThreshold)是辅助手段,但不能替代中间件层的请求维度聚合 - 避免在中间件里
deferDB close ——*sql.DB是连接池,不是单次连接
统计指标落地时容易被忽略的细节
上报不是终点,字段设计决定你能否快速定位问题。比如只记“平均耗时”没用,必须拆出 P90/P99、error rate、rows affected 分布;而且 Gin 的 c.FullPath() 和 c.Request.URL.Path 在有通配符时返回不同结果,前者是注册路径(如 /users/:id),后者是真实路径(如 /users/123)——选错就让指标无法聚合。
- 用
c.FullPath()做指标分组,避免路径爆炸 - SQL 语句只截取前 64 字节 + hash,不存全文(安全 & 存储成本)
- 状态码必须从
c.Writer.Status()读,不是c.Writer.Status字段——后者未写入前是 0 - 如果用了异步上报(如
go func(){...}()),确保c.Copy()或提取必要字段,否则闭包访问已销毁的 context
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











