Go
Challenge 4 - Database
Write safe data with transaction and prepared statement.
Challenge: a safe transfer
You will write the function that handles a money transfer in a banking service. Use this interface instead of a real database:
type Tx interface {
Exec(sorgu string, args ...any) error
Commit() error
Rollback() error
}
Rules: the amount must be positive; the withdrawal and deposit must be in the same transaction; if any step fails, Rollback must be called; queries must use placeholders.
Testing with a fake Tx
Thanks to the interface you can write a fake Tx that records which calls were made:
type sahteTx struct {
log []string
hataAt int // which Exec should fail (0 = none)
}
func (t *sahteTx) Exec(q string, a ...any) error {
t.log = append(t.log, "exec")
if len(t.log) == t.hataAt { return errors.New("db hatası") }
return nil
}
In the test you verify that log is [exec exec commit] or [exec rollback].
Exercise The transfer function
Write transfer(tx Tx, kimden, kime, tutar int) error following the rules. Run two scenarios with the fake Tx: no error, and an error on the second Exec. Print each scenario's log on its own line.
Show example solution
package main
import (
"errors"
"fmt"
)
type Tx interface {
Exec(sorgu string, args ...any) error
Commit() error
Rollback() error
}
type sahteTx struct {
log []string
hataAt int
}
func (t *sahteTx) Exec(q string, a ...any) error {
t.log = append(t.log, "exec")
if len(t.log) == t.hataAt {
return errors.New("db hatası")
}
return nil
}
func (t *sahteTx) Commit() error { t.log = append(t.log, "commit"); return nil }
func (t *sahteTx) Rollback() error { t.log = append(t.log, "rollback"); return nil }
func transfer(tx Tx, kimden, kime, tutar int) error {
if tutar <= 0 {
return errors.New("geçersiz tutar")
}
if err := tx.Exec("UPDATE hesap SET bakiye = bakiye - ? WHERE id = ?", tutar, kimden); err != nil {
tx.Rollback()
return err
}
if err := tx.Exec("UPDATE hesap SET bakiye = bakiye + ? WHERE id = ?", tutar, kime); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
func main() {
a := &sahteTx{}
transfer(a, 1, 2, 100)
b := &sahteTx{hataAt: 2}
transfer(b, 1, 2, 100)
fmt.Println(a.log)
fmt.Println(b.log)
}
Test yourself
-
If the amount is negative, what is the right behavior?
- Return an error without touching the transaction
- Withdraw first, then check
- Use the absolute value of the amount
- Commit and log a warning
Show answer
Return an error without touching the transaction — Validation happens before any work; with invalid input no database operation should start.
-
Roll back on error
if err := tx.Exec("UPDATE hesap SET bakiye = bakiye - ? WHERE id = ?", tutar, kimden); err != nil {
tx.____()
return err
}
Show answer
if err := tx.Exec("UPDATE hesap SET bakiye = bakiye - ? WHERE id = ?", tutar, kimden); err != nil {
tx.Rollback()
return err
}
-
Case and outcome
- Amount ≤ 0
- Error, don't touch Tx
- Exec error
- Rollback
- Query parameter
- The ? placeholder
- Both succeed
- Commit
Next lesson: Advanced section exam