Ответ 1
Зачем вам это нужно? Функция, вызывающая Begin()
, должна также вызывать Commit()
или Rollback()
и возвращать соответствующую ошибку.
Например, этот код выполняет фиксацию или откат в зависимости от того, возвращается ли ошибка:
func (s Service) DoSomething() (err error) {
tx, err := s.db.Begin()
if err != nil {
return
}
defer func() {
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
}()
if _, err = tx.Exec(...); err != nil {
return
}
if _, err = tx.Exec(...); err != nil {
return
}
// ...
return
}
Обратите внимание, как я проверяю error
, чтобы узнать, следует ли мне совершать или откатывать. Однако приведенный выше пример не обрабатывает паники.
Мне не нравится делать логику commit/rollback для каждой процедуры базы данных, поэтому я обычно обертываю их в обработчик транзакций. Что-то вроде этого:
func Transact(db *sql.DB, txFunc func(*sql.Tx) error) (err error) {
tx, err := db.Begin()
if err != nil {
return
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p) // re-throw panic after Rollback
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
}
}()
err = txFunc(tx)
return err
}
Это позволяет мне сделать это вместо:
func (s Service) DoSomething() error {
return Transact(s.db, func (tx *sql.Tx) error {
if _, err := tx.Exec(...); err != nil {
return err
}
if _, err := tx.Exec(...); err != nil {
return err
}
})
}
Обратите внимание, что если что-либо внутри моей паники транзакций автоматически обрабатывается обработчиком транзакций.
В моей реальной реализации я передаю интерфейс вместо * sql.Tx, чтобы предотвратить нежелательные вызовы Commit()
или Rollback()
.
Вот простой фрагмент, демонстрирующий, как работает defer
(печатает 4, а не 5):
package main
func test() (i int) {
defer func() {
i = 4
}()
return 5
}
func main() {
println(test())
}