应避免将*sql.db存入context.withvalue,因其导致panic、ide无法补全、mock困难;正确做法是通过构造函数或结构体显式传入,全局复用db实例,并在query等操作中传入context.context。

别把*sql.DB塞进context.WithValue
运行时 panic、IDE 无法补全、测试时 mock 不进去——这些不是偶然,是直接用 context.WithValue(ctx, dbKey, db) 的必然结果。
context 不是对象容器,它不管理生命周期,也不做类型安全检查。取值时必须写 v, ok := ctx.Value(dbKey).(*sql.DB),一旦 ok 为 false,程序就崩。
更隐蔽的问题是:HTTP 中间件里设了一次,下游 handler 又设一次同 key,后者覆盖前者,但调用链里谁设谁读完全不可控。
真正该放进去的只有 userID、traceID 这类轻量只读元数据,且 key 必须是未导出类型(如 type dbKey struct{}),禁用字符串。
用闭包或构造函数显式传*sql.DB
最简单也最可靠的方式,是让 handler 自己持有依赖:
func NewUserHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { /* 直接用 db */ } }- 或者封装成结构体:
type UserRepo struct { db *sql.DB },再提供NewUserRepo(db *sql.DB) *UserRepo
*sql.DB 实例都应全局复用,绝不能在每个 handler 或每个请求里 sql.Open —— 否则连接池失效,数据库瞬间被打满。
Query 时才传context.Context,不是传*sql.DB
*sql.DB 是长生命周期对象,context.Context 是短生命周期信号。二者职责完全不同,混用就是错配。
真正要传 context 的地方,是数据库操作方法调用时:
- 用
db.QueryContext(ctx, ...)替代db.Query(...) - 用
tx.CommitContext(ctx)替代tx.Commit() - HTTP handler 中直接用
r.Context(),它已自带客户端断开自动 cancel
defer cancel();否则 goroutine 泄漏风险极高。
MySQL 驱动能响应 cancel,SQLite 不能,所以超时主要靠 deadline 触发错误返回,而非服务端中断。
Wire 或手工注入时,参数必须是接口
如果项目用了 Wire 或类似 DI 工具,NewUserService 的签名必须是 NewUserService(repo UserRepo, logger Logger),而不是 NewUserService(db *sql.DB, log *zap.Logger)。
理由很实际:
-
UserRepo接口由 service 包定义,实现放在 repo 包,测试时可直接传&mockRepo{} - 若传具体类型
*sql.DB,等于把数据访问细节泄漏到业务层,违反关注点分离 - Wire 生成代码时,同类型多个实例(如主从库)必须用
wire.Struct(&App{}, "primaryDB", "replicaDB")显式标注字段名,否则 panic
ctx.Value 里等到 query 时才 panic。
复杂点从来不在怎么传,而在于谁该拥有这个 *sql.DB 的所有权和生命周期控制权。把它交给 context,等于把钥匙扔进下水道,还指望门自己锁上。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











