我正在尝试了解如何在 goland 中处理交易,并查看了许多不同的教程,其中以不同的方式安排了关闭交易。我有几个问题没有明确的答案。
(1) 据我了解,
defer tx.Rollback()
将在tx.Commit()
之后被调用,所以rollback
在这种情况下不会做任何事情?这样做有多好?
func WithTx(ctx context.Context) (err error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
//(1)
defer tx.Rollback()
// work
if err = tx.Commit(); err != nil {
return err
}
return nil
}
(2) 如果操作过程中可能出现
recover
,那么使用panic
有多好?我是否理解正确,如果有error
,那么recove
r将不会被执行(r == nil)`?
(3)提交过程中出现错误是否需要回滚?
(4)如何将交易转为延期更正确?
defer func() {tx.Rollback()}
或 defer func(ttx *sql.Tx) {ttx.Rollbac()}(tx)
func WithTx(ctx context.Context) (err error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
//(2)
if r := recover(); r != nil {
_ = tx.Rollback()
err = fmt.Errorf("exception")
} else if err != nil {
_ = tx.Rollback()
} else {
//(3)
if errCommit = tx.Commit(); err != nil {
_ = tx.Rollback()
err = errCommit
}
}
}() //(4)
// work with panic
panic("")
return nil
}
也许有一些关于如何正确回滚事务的常见做法?
大部分需要说的内容都包含在评论中,但让我们回顾一下:
panic
!=error
。不要惊慌,更不用说如果所说的惊慌用于指示发生了可恢复的“错误”。返回错误
参见上文:在这种情况下使用 panic
recover
不是一个好主意。这充其量是糟糕的风格。您正在滥用
panic
-recover
机制来实现一些隐约让人想起 try-catch
的东西取决于数据库。如果您无法提交交易,那么可能没有什么可以回滚的。检查您的数据库的文档。无论如何,您都会忽略
Rollback
如果发生错误你想回滚事务,而 panic
不是这样做的方法,那么你会怎么做呢?好吧,您可以在出现错误时对其进行处理,并随时退出/回滚:
func (s *Storage) FooTx(ctx context.Context, args ...any) error {
tx, err := s.db.BeginTx(ctx)
if err != nil {
return err
}
for _, arg := range args {
// perform some logic/prep work on the input
query, err := s.processFoo(arg)
if err != nil {
tx.Rollback()
return err
}
// execute query for this argument, rollback if it fails
if err := tx.Exec(query, arg); err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
// or
if err := tx.Commit(); err != nil {
tx.Rollback()
return err
}
return nil
}
现在这有点冗长,但很容易理解。我们可以使代码更加简洁,但代价是不太具体的错误处理和可读性:
// note: named return value
func (s *Storage) FooTx(ctx context.Context, args ...any) (err error) {
tx, err := s.db.BeginTx(ctx)
if err != nil {
return err
}
// now we have a transaction, if an error happens here, we assume a rollback is required
defer func() {
if err != nil {
tx.Rollback()
return
}
err = tx.Commit() // commit, set return error here
}()
var query string
for _, arg := range args {
// note: we're using =, not :=. We are assigning to the return variable named err
if query, err = s.processFoo(arg); err != nil {
return // err is set and not nil, the defer function will handle it
}
if err = tx.Exec(query, arg); err != nil {
return // again, err is assigned, the defer func will roll back the TX
}
}
// done, commit is handled in the defer
}
如果提交调用失败,你需要回滚,并且你仍然想使用 defer 的东西,那么上面的函数应该这样写:
func (s *Storage) FooTx(ctx context.Context, args ...any) (err error) {
tx, err := s.db.BeginTx(ctx)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback() // if we're returning an error, rollback the transaction
}
}()
var query string
for _, arg := range args {
if query, err = s.processFoo(arg); err != nil {
return
}
if err = tx.Exec(query, arg); err != nil {
return
}
}
return tx.Commit() // sets the return variable to whatever Commit returns
}
这应该可行(未测试代码),但就我个人而言,我可能会选择第一个实现。拥有干净、易于阅读的代码比充满
defer
调用的代码更有价值。如果您开始添加
defer
调用来处理像处理提交和回滚数据库事务这样重要的事情,那么我会倾向于问为什么,并将其标记为代码气味 9/10 次。偶尔 defer
没有什么问题,例如:mu.Lock()
defer mu.Unlock()
// or
ctx, cfunc := context.WithCance(context.Background())
defer cfunc()
确保您不会忘记释放锁或取消上下文。我确实质疑将逻辑和错误处理放置到延迟函数中的决定,远离错误的来源或导致问题的逻辑。以节省 5-10 行代码为名的混淆通常意味着优先考虑的是错误的事情。
重定1.,
defer tx.Rollback()
用于“
执行事务”和数据库/sql中的示例代码,所以使用起来很安全。此外,在处理数据库连接时,额外调用的影响应该不明显。 这也回答了 3.,因为您可以返回任何错误,并且您的交易将自动回滚。
另请参阅。 4.,如您所见,
defer tx.Rollback()
是最容易阅读和您应该使用的。
考虑到 2.recover()
,您应该只恢复可以可靠处理的错误。由于您预计不会出现任何恐慌,因此您不应该处理它们。如果您的进程崩溃,服务器将自动回滚任何打开的事务。