go中无装饰器语法,需用高阶函数+闭包模拟事务包装:定义txfunc类型,withtx函数统一处理begin/commit/rollback及panic捕获,必须显式检查error、透传context、避免嵌套与并发操作。

Go 里没有装饰器语法,但可以用函数式包装模拟
Go 语言本身不支持 Python 那种 @decorator 语法,所谓“装饰器模式”在 Go 中实际是靠高阶函数 + 闭包实现的事务包装逻辑。核心思路是:把业务逻辑抽象成一个接受 *sql.Tx 的函数,再用外层函数负责开事务、传 *sql.Tx、捕获 panic、决定回滚或提交。
常见错误是直接在包装函数里调用 db.Begin() 后就传 *sql.Tx 进去,却没处理内部 panic 或 error 返回,导致事务既没提交也没回滚,连接卡住、锁不释放。
- 必须显式检查业务函数返回的
error,仅靠 defer 回滚不够 - 不能忽略
tx.Commit()和tx.Rollback()的返回值 —— 它们本身可能失败(比如网络断连) - 别在事务函数里再调用
db.Query等直连 db 的方法,必须统一走传入的*sql.Tx
用 func(*sql.Tx) error 类型统一事务逻辑入口
这是最轻量也最可控的方式。定义类型别名让签名清晰,也方便复用包装逻辑:
type TxFunc func(*sql.Tx) error
<p>func WithTx(db *sql.DB, f TxFunc) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
if err := f(tx); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}使用场景很明确:只要涉及多条写操作、需要原子性保障,比如「扣库存 + 写订单 + 记日志」,就该套这个壳。注意 WithTx 不会自动重试,也不处理隔离级别 —— 需要的话得手动在 db.BeginTx 里传 &sql.TxOptions{Isolation: sql.LevelSerializable}。
立即学习“go语言免费学习笔记(深入)”;
-
WithTx是阻塞调用,事务生命周期完全由它控制,别在外层再 defertx.Close(*sql.Tx没 Close 方法) - 如果业务函数里用了
context.WithTimeout,记得把 context 透传进db.BeginTx,否则超时控制失效 - 不要把
WithTx套在 HTTP handler 顶层 —— 一次请求多个事务应各自独立调用,避免长事务拖垮连接池
别信“自动提交”,Go 的 database/sql 默认根本不开事务
新手常误以为 db.Exec 或 db.Query 会自动包裹事务。事实是:每条语句都是独立执行的,没有隐式事务。所谓“自动提交”只是指语句执行完立刻生效,不是开启了一个可回滚的事务上下文。
典型翻车现场:db.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); db.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2") —— 中间出错,第一笔已扣款,第二笔失败,钱就丢了。
- MySQL 默认 autocommit=1,PostgreSQL 默认每个语句是事务,但 Go 的
database/sql层不做任何干预,全靠你手动控制 -
sql.Tx一旦 Commit 或 Rollback,对象即失效,再调用其方法会 panic "sql: Transaction has already been committed or rolled back" - 别用
defer tx.Rollback()代替条件判断 —— 它会在函数结束时无差别执行,大概率把本该提交的成功事务给回滚了
嵌套事务?Go 没原生支持,得靠保存点或拆逻辑
SQL 标准里的 SAVEPOINT 在 Go 里得手动拼 SQL 实现,database/sql 不提供 tx.Savepoint() 这类方法。想模拟“子事务失败只回滚局部”,要么手写 tx.Exec("SAVEPOINT sp1") 和 tx.Exec("ROLLBACK TO sp1"),要么把大事务拆成多个独立 WithTx 调用 + 外部状态协调。
性能上,SAVEPOINT 开销比完整事务小,但仍是额外 round-trip;兼容性上,SQLite 支持有限,TiDB 对嵌套 SAVEPOINT 行为和 MySQL 不完全一致。
- MySQL 8.0+ 的
START TRANSACTION WITH CONSISTENT SNAPSHOT无法通过BeginTx直接触发,得用Exec手动发 - 别在同一个
*sql.Tx上并发执行多个查询 ——*sql.Tx不是线程安全的,会 panic "sql: Transaction is closed" - 真正难的不是写包装函数,而是判断哪些操作必须放同一事务、哪些可以异步补偿 —— 这取决于业务一致性要求,不是技术能自动解决的










