22

I have started using Go for a web-service and have some database interaction (surprise!!!) and I have found this example:

tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close() // danger!
for i := 0; i < 10; i++ {
    _, err = stmt.Exec(i)
    if err != nil {
        log.Fatal(err)
    }
}
err = tx.Commit()
if err != nil {
    log.Fatal(err)
}
// stmt.Close() runs here!

From http://go-database-sql.org/prepared.html

The example is well formulated an easy to understand. However, it leaves me with an unanswered question. Why defer the transaction Rollback call?

Why not just do the following:

err := tx.Commit()

if err != nil {
    log.Error(err)
    tx.Rollback()
}

Would defer tx.Rollback() not always attempt a rollback? Even if tx.Commit() was a success, or have I misunderstood something about defer?

Lars Nielsen
  • 2,005
  • 2
  • 25
  • 48
  • 3
    Looks like a mistake. As you say you wouldn't want to rollback unless an error occurred. Also the log.Fatalf you wouldn't want in a real app. I suggest use the improve this page button and contact the author. – Kenny Grant Sep 26 '17 at 08:47
  • 1
    Thank you for your answer :) – Lars Nielsen Sep 26 '17 at 09:19
  • @KennyGrant Thumb up for improving this misleading example. – aristotll Sep 06 '19 at 08:05
  • 3
    @KennyGrant Using `defer tx.Rollback()` after beginning a transaction can be nice to avoid putting rollback logic for every other error during the lifespan of the transaction. Note that if the transaction is already committed, calling rollback will perform a `NOP`. That being said, with verbosity comes more control, so if speed is a concern, handling every rollback rather than using `defer` may be preferable (or for verbose logging, etc). – Kelly Flet Apr 23 '20 at 17:09

2 Answers2

42

The important thing is that if you defer tx.Rollback() you are sure that it will be called even if you do an early return and the "trick" is that calling tx.Rollback() on a committed transaction will not actually do the rollback, because once a transaction is committed, it's committed and there is no way to roll it back :) So this is a neat trick on how to keep the code simple.

Tomor
  • 814
  • 6
  • 10
  • 3
    Would the additional rollback after a successful commit not result in a roundtrip to the server, regardless of the fact that it's effectively a NOP? If this is the case, and there are 100's of consecutive such (successful) operations, then this 'trick' would result in a performance hit.it. – bosvos May 14 '21 at 06:23
  • 5
    Hi @bosvos , it will not. You can check the Rollback() code in `database/sql/sql.go`. It does `atomic.CompareAndSwapInt32(&tx.done, 0, 1)` first and if the transaction is done it returns right away. – Tomor May 15 '21 at 13:07
8

The example is a little bit misleading. It uses log.Fatal(err) for error handling. You wouldn't normally do that, and instead return err. So the deferred rollback is there to ensure that the transaction is rolled back in case of an early return.

Peter
  • 29,454
  • 5
  • 48
  • 60