go中函数间传递数据库连接的核心是显式传参*sql.db,禁用全局变量、值传递及函数内close;因全局变量破坏可测试性、隐藏依赖、模糊资源生命周期,且易致连接关闭混乱或泄漏。

Go 中函数间传递数据库连接,核心就一条:用 *sql.DB 显式传参,别碰全局变量、别值传递、别在函数里 Close()。
为什么不能直接用全局 db 变量?
全局变量看似方便,但会破坏可测试性、隐藏依赖、增加耦合。更关键的是,它掩盖了资源生命周期问题——db.Close() 该在哪儿调?谁负责?一旦多个包都去 defer db.Close(),程序可能提前断连;没人调,又导致连接泄漏。Go 的设计哲学是“显式优于隐式”,db 是资源句柄,它的使用范围必须清晰可控。
常见错误现象:
-
sql: database is closed—— 某个函数提前关了db,其他函数再调用就 panic - 单元测试难 mock —— 全局变量无法被替换,只能真实连库
- 包初始化顺序引发 panic ——
init()中访问未初始化的全局db
*sql.DB 必须作为参数传,且类型要写对
sql.DB 本身是并发安全的连接池句柄,官方明确建议只初始化一次、长期复用。它内部已是指针语义,所以函数签名必须用 *sql.DB,不是 sql.DB(值类型)。
示例对比:
// ❌ 错误:值传递,编译失败或行为异常
func badQuery(db sql.DB) { ... }
// ✅ 正确:指针传递,符合惯用法
func goodQuery(db *sql.DB) {
rows, err := db.Query("SELECT name FROM users")
if err != nil {
// 处理 err
}
defer rows.Close()
// ...
}
要点:
- 所有需要 DB 的函数、方法、结构体字段,类型统一为
*sql.DB - 不要试图“解引用”再传,
db就是*sql.DB,直接传即可 - 如果封装了结构体(如
UserService),字段也应是DB *sql.DB
事务场景下,必须传 *sql.Tx,不能混用 *sql.DB
事务不是给 db “加开关”,而是派生出一个全新的上下文句柄 *sql.Tx。一旦进入事务,所有操作必须走这个 tx 实例,否则立刻落库,破坏一致性。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
常见错误:
- 在
db.Transaction()回调里,一边用tx.Query(),一边又写db.Exec() - 把
db传进函数,函数内部却自己调db.Begin(),导致嵌套事务或连接卡死
正确做法:
err := db.Transaction(func(tx *sql.Tx) error {
_, err := tx.Exec("UPDATE accounts SET balance = ? WHERE id = ?", newBal, id)
if err != nil {
return err // 自动回滚
}
return tx.Commit() // 不需要手动 Commit,Transaction 已处理
})
注意:
-
db.Transaction()是默认安全选择,自动捕获 panic、回滚 error、提交成功路径 - 只有需跨函数显式传递事务(如 service → repo 分层)或用
SavePoint时,才用db.Begin() -
tx也必须作为参数传入下游函数,不能在函数内重新从db派生
跨包共享时,别导出 db,改用构造函数注入
不同包之间共享连接,最干净的方式是:主包初始化 *sql.DB,然后通过构造函数(或选项函数)注入到其他包的结构体中。
例如 repo.UserRepo 不该自己连库,而应接收 *sql.DB:
type UserRepo struct {
db *sql.DB
}
func NewUserRepo(db *sql.DB) *UserRepo {
return &UserRepo{db: db}
}
func (r *UserRepo) FindByID(id int) (*User, error) {
return r.db.QueryRow("SELECT ...").Scan(...)
}
这样做的好处:
- 依赖关系一目了然,不靠 import 隐式绑定
- 测试时可轻松传入 mock
*sql.DB(如sqlmock) - 避免多个包各自
sql.Open(),造成连接池失控
真正容易被忽略的一点:连接池参数(SetMaxOpenConns、SetConnMaxLifetime 等)必须在 sql.Open() 之后、首次使用之前设置,且只能设一次。如果这些配置分散在不同包初始化逻辑里,极易遗漏或重复设置。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










