服务层传参必须用*gorm.DB指针类型,因值类型会导致编译错误、连接泄漏、钩子失效及事务上下文丢失;事务中须全程使用同一tx实例,禁用原始db,Begin后需显式Commit/Rollback并检查错误,嵌套调用要透传tx,I/O操作应移出事务体。

服务层传参必须用 *gorm.DB,不能用值类型
结构体字段或函数参数若声明为 gorm.DB(值类型),编译直接报错:cannot assign *gorm.DB to dc.DB (type gorm.DB)。因为 gorm.Open() 和 db.Transaction() 返回的全是 *gorm.DB,Go 不允许隐式指针转值。
更严重的是,gorm.DB 内部含连接池、回调链、日志器等状态,按值传递会导致连接泄漏、钩子失效、事务上下文丢失。所以从初始化、字段定义、到跨层调用,全程统一用指针:
type OrderService struct { DB *gorm.DB }func (s *OrderService) CreateOrder(tx *gorm.DB, order *Order) error- 调用方必须显式传入
tx,而非原始s.DB
db.Transaction() 里别再用原始 db 实例
常见错误是:在 db.Transaction(func(tx *gorm.DB) error { ... }) 闭包内,一边调 tx.Create(),一边又写 db.First()——后者完全脱离事务,数据立刻落库,原子性崩塌。
事务一致性只靠 tx 实例维持,它不是“给 db 加开关”,而是全新派生的带上下文句柄。进了 Transaction 就该彻底丢掉原始 db 变量:
- ✅ 正确:
tx.Preload("Items").First(&order)、tx.Model(&user).Update("balance", 100) - ❌ 危险:
db.Create(&log)、db.Where("id = ?", id).Delete(&item)
手动 Begin() 后必须配对 Commit() 或 Rollback()
db.Begin() 返回新 *gorm.DB,但不会自动管理生命周期。漏掉 Commit() 或 Rollback(),连接永不释放,连接池很快耗尽。
别依赖 defer tx.Rollback() 简单兜底——它无法判断是否已成功 Commit(),反而会在提交后错误回滚。正确做法是:
- 用
defer func() { if r := recover(); r != nil { tx.Rollback() } }()捕获 panic - 每个分支都显式处理错误并调
tx.Rollback(),成功路径调tx.Commit() -
tx.Commit().Error必须检查,忽略等于放弃事务结果
嵌套调用时,tx 要一路透传,不能中途换实例
GORM 没有真正嵌套事务,tx.Begin() 在已有事务中只是建 SAVEPOINT。但如果你在 service 层调 tx.Begin(),又把新返回的 *gorm.DB 传给 repo,而 repo 内部又误用 db.Session() 或 db.WithContext(),就会生成一个脱离外层事务的新实例,导致保存点失效、回滚失败、报 no such savepoint。
关键约束只有一个:所有操作必须链在同一 tx 实例上。哪怕跨 service → repo → dao 多层,参数也只传这个 tx,不新建、不转换、不缓存。
最容易被忽略的是 HTTP 调用、日志上报、缓存写入这些 I/O 操作——它们若内部用了独立 *sql.DB,就天然游离在事务 context 之外,且不受超时控制。这类逻辑必须移出事务体,改用 tx.AfterCommit() 异步触发,或走消息表补偿。











