必须在gorm.Open之后调用db.Use(dbresolver.Register()),否则插件不生效;主从DSN须分开配置,事务内及强一致读需显式标记走主库。

dbresolver.Register 必须在 gorm.Open 之后调用
插件不生效是最常见的“读写分离没起作用”原因。GORM 不会在初始化前加载插件,db.Use(dbresolver.Register()) 放在 gorm.Open 前等于白写。
正确顺序必须是:
- 先调
gorm.Open(dialector, &gorm.Config{})得到基础*gorm.DB - 再立刻调
db.Use(dbresolver.Register()) - 最后才配置主从连接池(
dbresolver.Register的参数)
漏掉第二步,所有 Find、First 都静默走主库——你查日志、看 SQL 执行 IP 都发现不了异常,因为不报错也不警告。
主库 DSN 和从库 DSN 必须分开传,不能拼成一个字符串
gorm.Open 只接受一个 DSN,且它只用于初始化基础连接对象。所谓“主从地址写在一起”,比如 "mysql://user:pass@master:3306/db?slave=slave1:3306,slave2:3306",驱动根本不识别,会直接报 invalid connection 或解析失败。
立即学习“go语言免费学习笔记(深入)”;
实操上:
-
gorm.Open的 DSN 必须只填主库地址(哪怕你暂时只有一个库) - 每个从库 DSN 要单独调
db.AddSlave()注册,且必须检查返回的error - 从库 DSN 中必须已开启
read_only=1(MySQL)或对应只读配置,否则SELECT FOR UPDATE一执行就报ERROR 1290
注册失败时 GORM 不 panic,但后续读请求全 fallback 到主库,压测时容易突然雪崩。
事务内所有操作强制走主库,和 SQL 内容无关
别信“我在事务里只写了 SELECT,应该能走从库”这种想法。GORM 的路由逻辑只看调用栈是否在 *sql.Tx 或 db.Begin() 后,不解析 SQL 文本。
典型错误场景:
-
tx := db.Begin()后,又调slaveDB.QueryRow("SELECT ...")—— 这不是事务查询,是另一个独立会话,数据可能不一致 - ORM 自动生成的关联查询(如
Preload)在事务中仍走主库,但若你在事务外用db.Preload().Find(),默认走从库 -
Raw("SELECT ... FOR UPDATE")默认走从库,执行即挂;必须显式加.Clauses(dbresolver.Write)或改用tx.Raw()
事务起点必须是主库 *gorm.DB 实例,不能从从库实例 Begin() —— 从库不支持事务启动。
强一致读必须手动标记,不能依赖“刚写完就自动读主库”
GORM 不做任何写后读一致性保证。你 Create 完立刻 First,如果没干预,默认还是发往从库,大概率查不到。
安全做法只有两种:
- 显式加写意图:
db.Session(&gorm.Session{Write: true}).First(&u) - 或用 Clauses:
db.Clauses(dbresolver.Write).First(&u)
注意:db.Unscoped()、db.Clauses(clause.Locking{Strength: "UPDATE"}) 等带锁语义的操作也会自动退回到主库,但别依赖这个——语义不明确时,显式标记更可靠。
连接池、超时、健康检查都得为主从库分别配置,共用 SetMaxOpenConns 或忽略 Ping() 校验,上线后必出问题:主库连接数爆满,从库空转。主库要设短 WriteTimeout,从库可放宽 ReadTimeout;每个 *sql.DB 实例都要单独 defer db.Close(),尤其 CLI 工具里最容易漏。


















