gorm不原生支持只读事务,必须通过db.begin()获取事务句柄后执行原生sql“set transaction read only”手动开启,且不能在db.transaction()闭包中动态设置,因数据库禁止事务中切换模式、postgresql要求read only为第一条语句、gorm事务不保证连接复用。

只读事务在 GORM 中不被原生支持
GORM 没有 BeginReadOnly 或类似 SET TRANSACTION READ ONLY 的封装函数。所谓“只读事务”,本质是数据库层面的会话级设置,GORM 本身不提供开箱即用的只读事务 API。你无法靠 db.Transaction() 或 db.Begin() 直接声明一个只读上下文——它默认仍是可读写的,只是你选择不写。
手动开启只读事务必须用 raw SQL + Begin
要真正启用数据库级只读事务(例如防止误更新、触发 MySQL 的只读优化、配合从库路由),只能绕过 GORM 封装,用底层连接执行 SQL:
- 先调用
db.Begin()获取裸事务句柄 - 再对
tx执行原生 SQL:tx.Exec("SET TRANSACTION READ ONLY") - 后续所有
tx.Query/tx.Raw操作都在该只读上下文中 - 最后必须显式
tx.Commit()或tx.Rollback(),否则连接泄漏
示例:
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Exec("SET TRANSACTION READ ONLY").Error; err != nil {
tx.Rollback()
return err
}
var users []User
if err := tx.Find(&users).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit()
为什么不能在 Transaction() 闭包里设只读
db.Transaction() 内部使用的是短生命周期事务,且不暴露底层连接控制权。你在闭包里执行 tx.Exec("SET TRANSACTION READ ONLY") 可能成功,但:
- MySQL 不允许在已开启的事务中动态切换只读模式(报错
ERROR 1792 (25006): Cannot execute statement in a READ ONLY transaction) - PostgreSQL 要求
BEGIN READ ONLY必须作为事务启动的第一条语句,而db.Transaction()已经发起了BEGIN - GORM 的
tx实例不保证复用同一物理连接,SET命令可能作用于错误连接
更安全的替代方案:用 Session + Context 控制读写意图
与其强行模拟只读事务,不如从设计上隔离读写路径:
- 为只读查询单独配置一个只连从库的
*gorm.DB实例(如slaveDB),并禁用写操作:slaveDB.Config.SkipDefaultTransaction = true - 在 Gin handler 中明确区分:
slaveDB.Find(&u)vsmasterDB.Create(&u) - 若需强一致性读,改用
tx.Session(&gorm.Session{ReadCommitted: true})而非只读事务
真正的只读约束应由数据库角色权限(如只授予 SELECT)和连接池路由策略兜底,而不是靠 Go 层事务包装器假装安全。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











