Golang多DAO复用同一数据库事务实现咨询——类似Java@Transactional的事务回滚需求
解决Go中事务回滚与方法独立调用的冲突问题
这个问题我之前也碰到过——既要在多表更新时保证事务原子性(失败就回滚),又要让单个更新方法能独立调用,确实需要调整下实现方式。下面给你两种常用的优雅解决方案:
方案一:用接口抽象SQL执行器
Go的sql.DB和sql.Tx都实现了ExecContext、QueryContext等方法,我们可以定义一个通用接口来统一这两者的行为,这样更新方法就不用关心当前是在事务内还是独立执行。
步骤1:定义通用执行器接口
// SQLExecutor 抽象DB和Tx的通用执行方法 type SQLExecutor interface { ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) // 如果你的业务需要查询,也可以加上QueryContext方法 // QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) }
步骤2:修改更新方法接收接口参数
把原来的Update1、Update2改成接收SQLExecutor类型的参数,同时改用带Context的方法(推荐所有数据库操作都用Context,方便超时控制):
func (d *dao1) Update1(ctx context.Context, exec SQLExecutor) error { query := "update T1 set C1 = ? where id = ?" _, err := exec.ExecContext(ctx, query, "v1", "v2") return err } func (d *dao2) Update2(ctx context.Context, exec SQLExecutor) error { query := "update T2 set C2 = ? where id = ?" _, err := exec.ExecContext(ctx, query, "v1", "v2") return err }
步骤3:事务内与独立调用的实现
- 事务内调用:传入开启的事务
tx
func (d *dao) Method1(ctx context.Context, opt string) error { tx, err := d.DB.BeginTx(ctx, nil) if err != nil { return errors.Errorf("unable to start transaction: %w", err) } // 用defer统一处理事务提交/回滚,兼顾panic和error场景 defer func() { if r := recover(); r != nil { _ = tx.Rollback() panic(r) // 恢复panic,让上层处理 } else if err != nil { _ = tx.Rollback() log.Infof("transaction rolled back due to error: %v", err) } else { err = tx.Commit() if err != nil { log.Errorf("failed to commit transaction: %v", err) } } }() // 传入tx执行更新 err = d.dao1.Update1(ctx, tx) if err != nil { log.Errorf("failed to update T1: %v", err) return err } err = d.dao2.Update2(ctx, tx) if err != nil { log.Errorf("failed to update T2: %v", err) return err } return nil }
- 独立调用:直接传入DAO的
DB实例
// 单独调用Update1 err := d.dao1.Update1(context.Background(), d.dao1.DB) if err != nil { // 处理错误 }
方案二:给DAO添加可选事务字段
如果觉得传接口参数有点麻烦,也可以给DAO结构体加一个可选的Tx字段,执行时优先使用Tx,否则用DB。
步骤1:修改DAO结构体
type dao1 struct { DB *sql.DB Tx *sql.Tx // 非nil时使用事务执行,nil时用DB } type dao2 struct { DB *sql.DB Tx *sql.Tx }
步骤2:更新方法根据Tx字段选择执行器
func (d *dao1) Update1(ctx context.Context) error { query := "update T1 set C1 = ? where id = ?" var err error if d.Tx != nil { _, err = d.Tx.ExecContext(ctx, query, "v1", "v2") } else { _, err = d.DB.ExecContext(ctx, query, "v1", "v2") } return err } func (d *dao2) Update2(ctx context.Context) error { query := "update T2 set C2 = ? where id = ?" var err error if d.Tx != nil { _, err = d.Tx.ExecContext(ctx, query, "v1", "v2") } else { _, err = d.DB.ExecContext(ctx, query, "v1", "v2") } return err }
步骤3:事务内与独立调用的实现
- 事务内调用:创建带Tx的DAO副本(避免修改原DAO的Tx字段,影响其他调用)
func (d *dao) Method1(ctx context.Context, opt string) error { tx, err := d.DB.BeginTx(ctx, nil) if err != nil { return errors.Errorf("unable to start transaction: %w", err) } defer func() { if r := recover(); r != nil { _ = tx.Rollback() panic(r) } else if err != nil { _ = tx.Rollback() log.Infof("transaction rolled back due to error: %v", err) } else { err = tx.Commit() if err != nil { log.Errorf("failed to commit transaction: %v", err) } } }() // 创建带事务的dao1和dao2实例 txDAO1 := &dao1{DB: d.dao1.DB, Tx: tx} err = txDAO1.Update1(ctx) if err != nil { log.Errorf("failed to update T1: %v", err) return err } txDAO2 := &dao2{DB: d.dao2.DB, Tx: tx} err = txDAO2.Update2(ctx) if err != nil { log.Errorf("failed to update T2: %v", err) return err } return nil }
- 独立调用:直接使用原DAO实例即可(Tx为nil,自动用DB执行)
err := d.dao1.Update1(context.Background()) if err != nil { // 处理错误 }
一些关键注意事项
- 一定要使用
ExecContext、BeginTx等带Context的方法,这样可以通过Context控制超时、取消请求,避免长时间占用数据库连接。 - 事务的提交/回滚一定要放在
defer里,并且要处理panic场景(比如代码中出现未捕获的panic,也要回滚事务)。 - 错误包装用
%w(Go 1.13+支持),这样上层可以用errors.Is、errors.As判断错误类型。
内容的提问来源于stack exchange,提问作者I_dont_know
相关产品推荐
相关产品推荐

