结构体字段必须声明为 *gorm.DB,gorm.Open() 返回指针且 gorm.DB 不可按值传递;事务中须全程使用 tx,禁止混用 db;优先用 db.Transaction() 而非手动 Begin();性能敏感场景可禁用默认事务,但仅限无一致性要求的操作。

结构体字段必须声明为 *gorm.DB,否则编译失败
很多人在封装数据库控制器时,把字段写成 DB gorm.DB(值类型),然后调用 gorm.Open() 赋值,结果直接报错:cannot assign *gorm.DB to dc.DB (type gorm.DB)。这是因为 gorm.Open() 返回的是指针 *gorm.DB,而 Go 不允许隐式转换指针到值类型。
gorm.DB 内部包含连接池、回调链、日志器等大量状态,按值传递不仅非法,还会导致行为异常(比如连接泄漏、钩子失效)。所以从初始化、字段定义、方法返回,全部要统一用指针语义:
type DBController struct { DB *gorm.DB }dc.DB, err = gorm.Open(...)func (dc *DBController) GetDB() *gorm.DB { return dc.DB }
事务内不能混用 db 和 tx,所有操作必须链在 tx 上
常见错误是:在 db.Transaction() 闭包里,一边调 tx.Create(),一边又写 db.First() —— 后者完全脱离事务上下文,数据会立刻落库,破坏原子性。
事务一致性只靠 tx 实例维持,它不是“给 db 加个事务开关”,而是全新派生的、带上下文的句柄。一旦进了 Transaction 回调,就该彻底丢掉原始 db 变量:
- ✅ 正确:
tx.Where("id = ?", id).Updates(&stock)、tx.Preload("Items").First(&order) - ❌ 危险:
db.Create(&log)、db.Model(&user).Update("balance", newBal) - 跨函数调用时,参数必须传
*gorm.DB(即tx),不能传原始db
db.Transaction() 是默认安全选择,别手动 Begin() 除非真需要 SavePoint
90% 的业务场景(如下单+扣库存+记日志)应该直接用 db.Transaction()。它自动捕获 panic、自动回滚非 nil error、自动提交成功路径,不用写 defer tx.Rollback(),也不用担心忘记 Commit() 导致连接卡死。
Begin() 只适合两种情况:一是需要跨多个函数显式传递事务对象(比如 service → repo 分层),二是要用 SavePoint 做局部回滚(例如外层事务不变,只撤回某一步校验失败的操作)。硬套 Begin() 容易漏 Commit() 或 Rollback(),连接池很快耗尽。
-
tx := db.Begin()后,必须配对tx.Commit()或tx.Rollback(),否则连接永不释放 -
tx.SavePoint("sp1")+tx.RollbackTo("sp1")是唯一合法的“嵌套”形式,不是真正嵌套事务 -
db.Transaction()内部不能再次调tx.Begin(),会 panic
性能敏感时可关默认事务,但仅限无一致性要求的写入
GORM 默认对每个 Create/Update/Delete 都包一层隐式事务,保证单条语句原子性,但带来约 30% 性能损耗。如果写的是日志表、埋点、缓存刷新这类“尽力而为”的操作,可以初始化时加 SkipDefaultTransaction: true。
注意:关掉后 db.Create() 就是直连执行,失败不回滚;但 db.Transaction() 依然可用,且不受影响。别全局关掉再手动补 Begin()——这等于自己重写一套易出错的事务调度逻辑。
- 适用:
db.Create(&accessLog)、db.Model(&cache).Updates(...) - 禁用:
db.Transaction()仍应保留用于核心业务流 - 风险:关掉后单条语句失败不会触发任何回滚,得靠上层业务兜底
事务最脆弱的地方不在语法,而在「上下文传递」——tx 一旦离开闭包或被意外替换,一致性就断了。别依赖注释提醒自己“这里要用 tx”,而要在函数签名、参数名、IDE 类型检查层面就杜绝 db 混入。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











