Transaktionklammer in Go

Mit Closures kann man ziemlich lustige Sachen machen in Go. Beispielsweise transaktionalen Code klammern:

Eine nichtsnutzige Transaktion

Um das Beispiel einfach zu halten, bauen wir uns einen Transaktionstyp der ein bisschen so aussieht, wie der echte aus der Db lib.

 9
10
11
12
13
14
15
16
17
18
19
20
type transaction struct{}

func (t *transaction) exec(s string) error {
	fmt.Println("exec: ", s)
	return nil
}
func (t *transaction) commit() {
	fmt.Println("commit")
}
func (t *transaction) rollback() {
	fmt.Println("rollback")
}

So weit so unspektakulär:

Ich brauche eine Transaktion, damit ich etwas machen kann (exec) und wenn ich mit allem fertig bin, möchte ich die Transaktion wieder schließen.

Ist etwas schief gegangen, soll ein rollback erfolgen, so dass keine Änderungen in der Datenbank landen, ist alles ok, wird die Transaktion mit einem commit abgeschlossen.

Die Transaktionsklammer

Dafür könnte man eine Wrapper Funktion bauen:

23
24
25
26
27
28
29
30
31
32
33
34
func withTx(f func(*transaction) error) {
	tx := &transaction{}
	var err error
	defer func() {
		if err == nil {
			tx.commit()
		} else {
			tx.rollback()
		}
	}()
	err = f(tx)
}
  • 24: erstellt die Transaktion
  • 25: definiert die Variable, die den Fehlerstatus halten soll. Da sie in der Closure von defer verwendet wird, muss sie bekannt sein, bevor sie referenziert wird.
  • 26-32: je nachdem ob err gesetzt ist, wird die Transaktion mit rollback oder commit finalisiert. Da es im defer ist, passiert das auch, wenn irgendwer mit einer panic aussteigt.
  • 33: ruft die Funktion auf und übergeben die Transaktion

Die Funktion withTx könnte irgendwo in einem DB Paket liegen.

Anwendung

Wie verwende ich das ganze nun?

Ich klammere einfach meinen transaktionalen Code mit der withTx Funktion und einer Closure:

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func main() {

	s1 := "first"
	s2 := "second"

	withTx(func(t *transaction) error {
		err := t.exec(s1)
		return err
	})

	withTx(func(t *transaction) error {
		err := t.exec(s2)
		err = errors.New("fail") // force fail
		return err
	})
}

Die Transaktion bekomme ich übergeben und ich muss lediglich err zurückgeben. Ich finde, das ist lesbarer, als wenn man den Code zum erzeugen und schließen der Transaktion n-fach kopiert.

In Playground ausprobieren