必须为每个数据源创建独立的gorm.DB实例,因为gorm.DB封装了*sql.DB、回调链、日志器和连接池等不可变状态,复用同一变量会导致连接泄漏、事务失效、中间件污染及配置错乱。

不能靠改一个 *gorm.DB 变量来“切换”数据库——每个库必须是独立的 *gorm.DB 实例,否则事务失效、连接池错乱、日志和回调互相污染。
为什么 gorm.Open 不能复用同一个变量
GORM 的 *gorm.DB 不是轻量上下文,它内部封装了 *sql.DB、回调链(Callbacks)、日志器(Logger)、插件(如 prometheus、opentelemetry)和连接池配置。两个不同 DSN 的数据库,哪怕只是库名不同,也必须调用两次 gorm.Open,分别赋值给不同变量(如 userDB、logDB)。
- 复用同一变量(比如
db = gorm.Open(...)覆盖赋值)会导致前一个库的连接池未关闭,连接泄漏 - 事务只在单个
*gorm.DB内有效:userDB.Transaction()中调用logDB.Create(),后者完全游离在事务外 - 驱动导入漏下划线(
import _ "gorm.io/driver/mysql"缺少_)时,gorm.Open不报错,但首次查询卡死或返回"invalid connection"
如何安全注册多个 GORM 实例
启动时显式初始化,避免 init() 隐式加载;每个实例用唯一变量名,并单独配置连接池与中间件。
- 用工厂函数替代全局
var:例如NewUserDB(cfg) (*gorm.DB, error),错误可透出、便于测试 - DSN 特殊字符必须编码:PostgreSQL 密码含
/或@时,需用url.QueryEscape处理,否则报"pq: password authentication failed"且难以定位 - 每个
*gorm.DB单独加中间件:userDB.Use(logger.New(...)),别用gorm.DefaultCallback全局注册,否则所有库共享同一日志行为 - 模型迁移必须按库执行:
userDB.AutoMigrate(&User{})和logDB.AutoMigrate(&Log{})是两套独立操作
运行时动态路由到对应 GORM 实例
靠 context.Context 透传目标库标识,由统一路由层(非 ORM 层)解析并返回对应 *gorm.DB,禁止在 DAO 层硬编码 if tenant == "shanghai" { return userDB } else { return pgDB }。
立即学习“go语言免费学习笔记(深入)”;
- 租户识别必须在入口中间件完成:从
r.Host、r.Header.Get("X-Tenant-ID")或 JWT claim 提取,查主库确认存在后才继续 - 缓存用
sync.Map或带 TTL 的内存缓存(如gcache),key 是租户 ID,value 是已Ping()验证过的*gorm.DB - handler 中必须显式取值:
db := ctx.Value(tenantDBKey).(*gorm.DB),不能 fallback 到全局变量或默认实例 - 别把路由逻辑塞进 GORM 回调里——
Create前临时换 DB,会破坏事务一致性,且无法控制SavePoint行为
读写分离必须由业务代码严格控制
GORM 没有“自动读从库”机制。所谓 db.Session(&gorm.Session{Write: true}) 或 db.Rebind() 在多数据源场景下行为不可控,且不跨库生效。
- 写操作(
CREATE/UPDATE/DELETE)必须用主库实例:masterDB.Create() - 读操作可走从库,但需确保不在事务内混用:
tx, _ := masterDB.BeginTx(ctx, nil)后,所有tx.QueryRow()必须用该tx,不能切到replicaDB - 不要抽象统一的
BeginTx接口:MySQL 在READ UNCOMMITTED下不支持SAVEPOINT,PostgreSQL 对隔离级别解析更严格,强行统一反而增加出错概率
最易被忽略的一点:GORM v2 要求显式传入 Dialector(如 mysql.Open(dsn)),不能只靠配置里的 driver: "mysql" 字符串推断——配置解析层必须匹配驱动类型,否则运行时报 "unsupported driver" 且堆栈不友好。


















