直接在 handler 里 new DB 连接会出问题,因为 sql.Open 仅创建连接池配置而非真实连接,首次 Query/Exec 才拨号;若未设 SetMaxOpenConns 和 SetConnMaxLifetime,连接易长期滞留、复用率低、耗尽数据库连接数;正确做法是全局单例初始化 *sql.DB 并共享复用。

为什么直接在 handler 里 new DB 连接会出问题
Go 模块里最常见错误是每次 HTTP 请求都 sql.Open 一个新连接,或在 struct 初始化时硬编码 db, _ := sql.Open(...)。这会导致连接泄漏、复用率低、超时不可控——sql.Open 只是创建连接池配置,并不真正建连;真正在第一次 db.Query 或 db.Exec 时才拨号,而如果没设 SetMaxOpenConns 和 SetConnMaxLifetime,连接可能长期滞留、占满数据库连接数。
正确做法是全局只初始化一次 *sql.DB,并通过依赖注入或包级变量暴露,让各层(repository、service、handler)共享同一个连接池实例。
- 所有数据库操作必须复用同一个
*sql.DB实例,不要在函数内重新sql.Open - 调用
db.SetMaxOpenConns(20)和db.SetConnMaxLifetime(10 * time.Minute)是必须项,不是可选项 - 避免在
init()函数里初始化*sql.DB,它无法返回 error,失败会静默 panic
如何分层暴露 DB 实例而不破坏封装
Go 没有类继承,但可以靠接口 + 构造函数实现松耦合。典型结构是:repository 层定义 type UserRepository interface { GetByID(id int) (*User, error) },service 层只依赖该接口,不碰 *sql.DB;而具体实现 userRepo 结构体持有一个 db *sql.DB 字段,在 NewUserRepository 时传入。
这样做的好处是:测试时可轻松 mock repository 接口,DB 不会渗透到 service 层逻辑里;升级数据库驱动(比如从 MySQL 换成 PostgreSQL)只需替换 repository 实现,上层完全无感。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- repository 接口方法参数和返回值不能含
*sql.DB或context.Context(后者应由调用方传入) - 构造函数如
NewUserRepository(db *sql.DB)必须校验db != nil,否则运行时 panic 难定位 - 不要把
*sql.DB塞进全局变量再到处 import,它会让单元测试无法隔离
context 传递与超时控制必须贯穿全链路
数据库操作不是孤立的,它必须响应上游 HTTP 请求的 deadline。如果 handler 传了 ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second),那这个 ctx 必须一路透传到 db.QueryContext(ctx, ...),否则 context.WithTimeout 形同虚设。
常见错误是 repository 方法签名写成 GetByID(id int) (*User, error),丢掉了 context;或者 service 层自己 new 一个 context.Background() 再传给 repository——这会让 HTTP 超时失效,请求卡住时无法中断 DB 查询。
- 所有涉及 I/O 的方法(包括 repository 的增删改查)签名必须带
ctx context.Context参数 - 不要在 repository 内部用
context.WithTimeout自行加超时,超时策略应由业务层(service 或 handler)统一决策 - 使用
db.QueryRowContext而非db.QueryRow,前者支持 cancel,后者会一直等直到 DB 返回或网络断开
事务管理该放在哪一层
事务边界必须由 service 层定义,repository 层只负责执行 SQL,不决定是否开启/提交/回滚。因为一个业务操作(比如“下单”)往往涉及多个 repository 协作(订单库 + 库存库 + 用户积分库),事务必须包裹整个操作,而不是单个 UserRepository.GetByID。
service 方法接收 ctx 和 *sql.Tx(或封装后的 Tx 接口),再把 tx 传给各 repository 方法;repository 接口需提供 WithTx(*sql.Tx) 方法或直接定义接受 tx *sql.Tx 的方法变体(如 CreateOrderTx(ctx, tx, order))。
- 禁止在 repository 内部调用
db.Begin(),这会让事务无法跨库协调 - service 层必须显式处理
tx.Commit()和tx.Rollback(),并确保 defer rollback 在 commit 成功后不再触发 - 如果用第三方 ORM(如 sqlx、ent),注意它们对
*sql.Tx的包装是否透明;有些会隐藏底层tx,导致你传进去的tx实际未生效
连接池参数、context 透传、事务边界——这三个点一旦漏掉任意一个,所谓“优雅管理”就只剩表象。实际项目里,90% 的数据库超时和连接耗尽,都源于其中某一条没落地。

















