gorm 的 db.connpool 不支持直接读写分离,因其默认将 db 实例视为单一连接池且无内置主从路由逻辑;需手动创建 masterdb 和 replicadb 两个实例分别处理写和只读操作,并严格避免混用。

为什么 GORM 的 DB.ConnPool 不支持直接读写分离
GORM v2 默认把 DB 实例当作单一连接池使用,所有操作(Create、First、Find 等)都走同一个 ConnPool。它本身没有内置“读请求自动发到从库、写请求发到主库”的路由逻辑。强行用 db.WithContext(context.WithValue(ctx, "role", "replica")) 这类方式做运行时判断,既不可靠又难维护——中间件或钩子无法覆盖所有调用路径,比如 Session、Transaction 或原生 Raw 查询会绕过你的逻辑。
用 gorm.Open 分别连接主从库,手动控制流向
最稳的方式是显式创建两个 *gorm.DB 实例:一个连主库(masterDB),一个连从库(replicaDB)。然后在业务层按需选择:
-
masterDB用于Create、Save、Delete、Transaction和任何需要强一致性的更新 -
replicaDB用于只读查询,如Find、First、Count、Joins(注意:带SELECT FOR UPDATE的查询必须走主库) - 避免混用:不要对
replicaDB调用Save,GORM 不会报错但数据库会返回ERROR: cannot execute INSERT in a read-only transaction
示例:
masterDB, _ := gorm.Open(postgres.Open(masterDSN), &gorm.Config{})
replicaDB, _ := gorm.Open(postgres.Open(replicaDSN), &gorm.Config{})
// 写
masterDB.Create(&user)
// 读(明确走从库)
var users []User
replicaDB.Find(&users)
// 复杂读 + 关联预加载(仍走从库)
replicaDB.Preload("Orders").Find(&users)
用 Session + WithContext 实现轻量级路由(仅限简单场景)
如果你已有大量代码基于单个 db 实例,且读写比例高、事务少,可用 Session 搭配上下文临时切换目标库。但这要求你统一管理查询入口,并接受部分边缘 case 无法覆盖:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 必须用
db.Session(&gorm.Session{Context: ctx})包裹每次调用,不能依赖全局db - 在中间件中注入 context key,例如
ctx = context.WithValue(ctx, dbRoleKey, "replica") - 自定义
Callback在BeforeQuery阶段检查 context,动态替换Statement.DB——但该 callback 不触发于Transaction内部或Raw查询 - 性能无额外开销,但调试困难:一旦漏传 context,读请求就落到主库,可能拖慢写吞吐
不推荐新项目采用此方式;老系统灰度迁移时可作为过渡手段。
事务内必须全程使用 masterDB,从库不参与事务
GORM 的 Transaction 本质是调用底层 SQL 的 BEGIN/COMMIT,而多数数据库(PostgreSQL、MySQL 主从)不允许在只读副本上开启事务。常见错误现象是:
-
replicaDB.Transaction(func(tx *gorm.DB) error { ... })直接 panic 或返回ERROR: cannot execute BEGIN in a read-only transaction - 即使没显式调用
Transaction,只要用了SavePoint或嵌套Session,也必须确保底层数连接来自主库
正确做法是:所有事务块必须基于 masterDB 构建,且事务内所有读操作(哪怕只是 tx.First)也走同一连接——否则会出现幻读或查不到刚插入的数据。
容易被忽略的一点:GORM 的 Preload 在事务内默认仍走原始 DB 的连接池,除非你显式用 tx.Preload(...)。所以务必确认 preload 调用对象是 tx,而非全局 replicaDB。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










