不能只用一个 *sql.DB 变量管理所有数据源,因其 driver 和 DSN 在 sql.Open() 时已绑定且不可更改,混用会导致事务混乱、连接泄漏或 panic。

Go 应用里要同时连多个数据库(比如 MySQL + PostgreSQL,或多个 MySQL 实例),不能靠全局 sql.DB 单例硬扛——它不支持运行时动态切换底层连接池,强行复用会导致事务混乱、连接泄漏、甚至 panic。
为什么不能只用一个 sql.DB 变量管理所有数据源
sql.DB 本身是连接池抽象,但它的 driver.Driver 和 dataSourceName 在 sql.Open() 时就已绑定,后续无法替换。常见误操作是写个 map 存一堆 *sql.DB,但忘了按需关闭、没做连接池隔离、或者在事务中混用不同实例。
- 多个业务模块共用同一
*sql.DB变量,却调用不同数据库的Open(),后一次会覆盖前一次,旧连接池变成孤儿 - 用
context.WithTimeout控制单次查询超时,但没传给db.QueryContext(),超时逻辑失效 - PostgreSQL 的
pgx驱动和 MySQL 的mysql驱动返回的*sql.DB类型虽一致,但底层协议、事务行为、错误码完全不同,不能交叉使用
用结构体封装多数据源 + 接口抽象统一访问
核心是把每个数据源独立初始化为私有字段,并通过接口暴露通用方法。避免直接暴露 *sql.DB,防止下游误用。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
type DBRouter struct {
mysql *sql.DB
pg *sql.DB
}
func (r *DBRouter) MySQL() *sql.DB { return r.mysql }
func (r *DBRouter) PG() *sql.DB { return r.pg }
// 更安全的做法:定义接口,隐藏具体驱动差异
type Queryer interface {
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}
- 初始化时分别调用
sql.Open("mysql", dsn1)和sql.Open("postgres", dsn2),各自设置SetMaxOpenConns、SetConnMaxLifetime - 不要在
DBRouter里实现BeginTx统一入口——MySQL 和 PostgreSQL 对 savepoint、isolation level 的支持程度不同,强行抽象反而增加出错概率 - 如果某模块只读 MySQL,就只注入
Queryer接口,而不是整个DBRouter,降低耦合
按请求上下文动态路由到指定数据源(如租户隔离)
不是所有多数据源都需要“手动选库”,有些场景(如 SaaS 多租户)要求根据 HTTP header 或 JWT claim 自动路由。这时别用 if-else 切换 *sql.DB,而应提前构建好映射表 + 延迟初始化。
立即学习“go语言免费学习笔记(深入)”;
var dbMap = sync.Map{} // key: tenantID, value: *sql.DB
func GetDBForTenant(tenantID string) (*sql.DB, error) {
if db, ok := dbMap.Load(tenantID); ok {
return db.(*sql.DB), nil
}
dsn := buildDSNByTenant(tenantID)
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// 设置连接池参数...
dbMap.Store(tenantID, db)
return db, nil
}
- 首次访问租户时才建连接池,避免启动时加载全部实例;但要注意
sync.Map不保证遍历顺序,不适合做健康检查 - 务必加
defer db.Close()在服务退出时清理,否则进程退出前未关闭的*sql.DB会卡住连接直到 OS 超时 - 若租户数达千级,建议用 LRU cache 替代
sync.Map,并定期驱逐空闲超过 30 分钟的连接池
真正麻烦的不是“怎么连多个库”,而是事务边界、连接泄漏检测、以及错误日志里分不清是哪个库抛的 ERROR: duplicate key value violates unique constraint。上线前至少用 go tool trace 看一眼各连接池的 wait duration 分布。

















