必须用context.WithValue()注入事务对象并替换请求上下文,因c.Set()仅是Echo内部map,database/sql和pgx等驱动只识别ctx.Value();漏掉c.SetRequest()则下游无法获取事务,导致隔离失效或panic。

必须把事务对象塞进 c.Request().Context(),而不是用 c.Set();否则下游 handler 或 DB 驱动无法感知事务状态,tx.Commit() 会失败或 panic。
为什么不能用 c.Set("tx", tx) 传事务
事务对象(如 *sql.Tx 或 pgx.Tx)需要被数据库操作函数(如 db.QueryRowContext()、tx.QueryRow())在调用时显式接收或从 context.Context 中提取。而 c.Set() 只是 Echo 自维护的一个 map,它不参与标准库 database/sql 或 pgx 的上下文传播逻辑——这些驱动只认 ctx.Value()。
- 常见错误现象:
c.Set("tx", tx)后,在 handler 里调用db.QueryRow("SELECT ...")仍走默认连接池,事务隔离失效 - 更隐蔽的问题:用
tx.QueryRow()能跑通,但换成db.QueryRowContext(c.Request().Context(), ...)就 panic —— 因为 context 里压根没 tx -
c.Get("tx")取出的值也无法直接传给QueryRowContext(),因为该方法只接受context.Context类型参数
正确做法:用自定义键 + context.WithValue() 注入
核心是两步:先构造带事务的 Context,再用 c.SetRequest() 替换原请求的 Context(Echo v4+ 必须)。
- 定义类型安全的键:
type txKey struct{}(避免字符串键冲突) - 中间件中注入:
newCtx := context.WithValue(c.Request().Context(), txKey{}, tx) - 关键一步:
c.SetRequest(c.Request().WithContext(newCtx))—— 漏掉这句,后续所有c.Request().Context()仍指向旧 context - handler 中取出:
if tx, ok := c.Request().Context().Value(txKey{}).(sql.Tx); ok { ... }
事务生命周期与优雅停机如何协同
事务不是“设进去就完事”,它必须和请求生命周期、服务关闭流程对齐;否则 shutdown 时未提交的 tx 会被强制中断。
- DB 驱动必须支持 context:用
pgx/v5或database/sql配sql.Open("pgx", ...),确保BeginTx(ctx, opts)和tx.Commit(ctx)可被超时控制 - shutdown 阶段要等事务结束:把
tx.Commit()或tx.Rollback()放进sync.WaitGroup,并在srv.Shutdown()前调用wg.Wait() - 别在中间件里
defer tx.Rollback():如果 handler panic,Echo 的Recover()中间件可能已提前恢复,导致 rollback 被跳过
最常被忽略的一点:事务注入不是单次动作,而是链路契约——所有依赖该事务的组件(日志审计、DB 查询、消息发送)都必须统一从 c.Request().Context() 取值,且 key 类型必须完全一致。任何一处用错 key 或漏掉 c.SetRequest(),整个事务链就断了。

















