我需要编写一个可重用的函数,其中的一部分可以在调用之前和之后运行。
例如:
isNewTnx, rollback, beginErr := DbConnectionManager.Begin(req)
if beginErr != nil {
return nil, beginErr
}
if isNewTnx {
defer rollback()
}
上面的代码启动一个事务,如果它是一个新事务,它会控制它,这意味着它可以是
commit
或 rollback
。我被迫在很多服务中编写这段代码。有什么办法可以让它更可重用吗?我知道函数柯里化是一种选择,还有其他选择吗?
以下代码使用
github.com/jmoiron/sqlx
中的类型作为示例。给定一个包装数据库驱动程序的数据结构(主要用于接口实现):
type Datastore struct {
*sqlx.DB
}
你在上面声明一个方法:
func (ds *Datastore) InTransaction(ctx context.Context, fn func(*sqlx.Tx) error) error {
tx, err := ds.BeginTxx(ctx, nil)
if err != nil {
return fmt.Errorf("open tx error: %w", err)
}
defer tx.Rollback()
if err := fn(tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit tx error: %w", err)
}
return nil
}
这里的技巧是始终使用
defer tx.Rollback()
进行回滚。如果事务成功完成,则回滚不会执行任何操作。如果发生错误,则会回滚事务。
然后你使用的是:
// ds := &Datastore{sqlx.MustConnect(.....)}
err := ds.InTransaction(ctx, func(tx *sqlx.Tx) error {
// actual application code where you run SQL statements using `tx`
})
对于需要返回某些内容并可能出现错误的函数,您可以使用
InTransaction
函数的通用包装器。不幸的是,这必须是一个采用数据存储结构(或任何适当的接口)的顶级函数,因为方法不能具有尚未在接收器类型上定义的类型参数:
// InTransaction is an adapter function to call InTransaction with an arbitrarily typed return pair.
func InTransaction[T any](ds *Datastore, ctx context.Context, fn func(tx *sqlx.Tx) (T, error)) (T, error) {
// this is the possibly empty typed value we want to return
var t T
// this is either the error produced by fn or the error produced by InTransaction
err := e.InTransaction(ctx, func(exec ExecStrategy) error {
result, err := fn(exec)
if err != nil {
return err
}
// propagate the typed value from fn out of InTransaction
t = result
return nil
})
// return t — possibly empty depending on what fn returned or whether it was called at all — and err
return t, err
}
然后你将其用作:
// ds := &Datastore{sqlx.MustConnect(.....)}
err := InTransaction(ds, ctx, func(tx *sqlx.Tx) (*MyType, error) {
// actual application code where you run SQL statements using `tx`
// and may return an arbitrary type and error
})