Go中可同时连多个数据库,但需为每个库独立初始化*sql.DB实例,分别配置连接池参数,避免混用导致查错库、事务失效或连接泄漏。

Go 里能同时连多个数据库,但不能靠“手动选库”硬切——sql.Open 返回的是独立连接池,gorm.Open 创建的是独立实例,混用或共享会导致查错库、事务失效、连接泄漏。关键不是“能不能”,而是“怎么管才不出事”。
多个 *sql.DB 实例必须各自独立初始化
每个数据库连接都该有自己专属的 *sql.DB,不能复用同一个变量或全局 var db *sql.DB。哪怕两个 MySQL 实例只差一个库名,也得分别 sql.Open。
-
sql.Open("mysql", dsn1)和sql.Open("mysql", dsn2)必须返回两个不同变量,比如userDB和logDB - 每个实例都要单独调用
SetMaxOpenConns、SetMaxIdleConns、SetConnMaxLifetime,否则某个库突发流量可能耗尽所有连接,拖垮其他库 - 别在
func init()里初始化 DB:错误无法透出,启动失败难定位;改用显式函数如NewUserDB(cfg) (*sql.DB, error)
用接口隔离具体驱动,避免中间件里 if-else 切库
如果某 handler 只读 PostgreSQL,就只注入 Queryer 接口,而不是整个 DBRouter 结构体。强行抽象 BeginTx 或统一 Exec 方法反而容易翻车——MySQL 不支持 SAVEPOINT 在某些隔离级别下生效,PostgreSQL 对 ISOLATION LEVEL 的解析也更严格。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 定义最小接口:
type Queryer interface { QueryRowContext(context.Context, string, ...any) *sql.Row; ExecContext(context.Context, string, ...any) (sql.Result, error) } - 中间件中从请求提取租户标识(如
r.Host或r.Header.Get("X-Tenant-ID")),查主库确认有效性后,从sync.Map缓存中取对应*sql.DB,再塞进context.WithValue - Handler 中用
db := ctx.Value(tenantDBKey).(*sql.DB)拿连接,而不是根据条件判断该用哪个变量
GORM 多实例共存的三个雷区
GORM 不是“多数据源框架”,它每个 *gorm.DB 都自带连接池、回调链、日志器。多个实例之间零共享,但误操作会让它们互相污染。
- 驱动导入漏了下划线:
import _ "gorm.io/driver/mysql"少那个_,gorm.Open不报错,第一次查询直接卡死或返回"invalid connection" - DSN 特殊字符未编码:PostgreSQL 用户名含
@或密码含/,必须用url.QueryEscape包一层,否则解析失败时只报"pq: password authentication failed",实际密码完全正确 - 事务跨库无效:写
userDB.Transaction(...)时调用logDB.Create(...),后者完全游离在事务外——GORM 做不了分布式事务,业务层得用补偿逻辑
连接泄漏和冷启动延迟比你想象中更常见
动态租户场景下,有人图省事用 map[string]*sql.DB 缓存租户连接,但没配 sync.Map + 懒加载 + 健康检查,结果新租户上线后首次请求超时,老租户删掉后连接还在内存里挂着。
- 缓存键建议用租户唯一标识(如
subdomain),不是 ID,避免 DNS 变更后查不到 - 每次从缓存取前先做
db.PingContext简单探活,失败则丢弃并重建,别等 query 时才发现连接断了 - 主库(tenant metadata DB)必须长期存活且配置宽松(如
SetConnMaxLifetime(0)),它是整个路由系统的单点,不能因空闲被关掉
最易被忽略的一点:所有数据库连接的 DSN 解析、驱动注册、连接池参数设置,必须在服务启动早期一次性做完。运行时再去 sql.Open 新库,尤其在高并发请求中触发,会成为性能瓶颈和故障放大器。

















