Thread-sicheres Struct-Design
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 65 von 107.
Da du nun Mutexes und WaitGroups verstehst, wollen wir sie kombinieren, um Structs zu entwerfen, die sicher von mehreren Goroutinen gleichzeitig verwendet werden können. Ein thread-sicheres Struct kapselt die Synchronisierung in seinen Methoden, sodass sich Aufrufer nicht um das Sperren kümmern müssen.
Das Muster ist unkompliziert: Bette einen Mutex in deine Struktur ein und sperre ihn in jeder Methode, die auf den gemeinsam genutzten Zustand zugreift:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}Beachte, dass sogar die schreibgeschützte Methode Value() den Mutex sperrt. Ohne dies könnte eine Goroutine lesen, während eine andere schreibt, was zu einer Data Race führen würde. Wenn Lesevorgänge weitaus häufiger als Schreibvorgänge sind, verwende stattdessen sync.RWMutex und rufe für Lesevorgänge RLock() auf.
Ein wichtiges Designprinzip: Halte den Mutex privat. Durch die Verwendung eines kleingeschriebenen Feldnamens (mu) verhinderst du, dass externer Code direkt darauf zugreift. Die gesamte Synchronisierung erfolgt über deine Methoden, sodass du die vollständige Kontrolle über die Thread-Sicherheit hast.
Bei Strukturen mit mehreren Feldern schützt du alle zusammengehörigen Felder mit demselben Mutex, um einen konsistenten Zustand sicherzustellen:
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}Sowohl balance als auch history werden atomar aktualisiert: Keine Goroutine kann einen inkonsistenten Zustand beobachten, in dem sich eines geändert hat, das andere jedoch nicht.
Aufgabe
EinfachErstellen wir ein threadsicheres Bankkontosystem, das die ordnungsgemäße Kapselung der Synchronisierung innerhalb von Struct-Methoden demonstriert. Dein Konto verarbeitet gleichzeitige Einzahlungen, Auszahlungen und Kontostandsabfragen sicher, ohne Aufrufern Details zur Sperrung offenzulegen.
Du organisierst deinen Code über zwei Dateien:
account.go: Definiere dein threadsicheres Bankkonto.Erstelle ein
BankAccount-Struct mit einem eingebettetensync.Mutex, einembalance-Feld (int) und einemtransactions-Slice, das alle erfolgreichen Vorgänge als Strings aufzeichnet.Implementiere diese Methoden:
NewBankAccount(initial int) *BankAccount– Erstellt ein neues Konto mit dem angegebenen Anfangssaldo und einem leeren transactions-SliceDeposit(amount int)– Addiert den Betrag zum Kontostand und zeichnet den Vorgang als+[amount]aufWithdraw(amount int) bool– Wenn ausreichende Mittel vorhanden sind, zieht die Methode den Betrag ab, zeichnet-[amount]auf und gibttruezurück. Andernfalls gibt siefalsezurück, ohne etwas zu ändernBalance() int– Gibt den aktuellen Kontostand zurückHistory() []string– Gibt eine Kopie des transactions-Slices zurück
Jede Methode, die auf die Felder des Structs zugreift, muss den Mutex sperren, um Thread-Sicherheit zu gewährleisten. Verwende
deferzum Entsperren. Halte den Mutex und alle Felder nicht exportiert (kleingeschrieben), damit externer Code deine Methoden verwenden muss.main.go: Verarbeite Bankvorgänge und demonstriere dein threadsicheres Konto.Lies den Anfangssaldo und anschließend die Anzahl der Vorgänge ein. Lies für jeden Vorgang den Typ (
deposit,withdrawoderbalance) und bei deposit/withdraw den Betrag ein.Gib für jeden Vorgang Ergebnisse aus:
deposit: GibDeposited [amount], Balance: [new balance]auswithdraw: Gib bei ErfolgWithdrew [amount], Balance: [new balance]aus oder andernfallsWithdrawal failed: insufficient fundsbalance: GibCurrent balance: [balance]aus
Gib nach allen Vorgängen den Transaktionsverlauf aus, wobei jeder Eintrag in einer neuen Zeile steht und nur dem ersten Eintrag
History:vorangestellt wird.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Anfangssaldo (Ganzzahl)
- Zeile 2: Anzahl der Vorgänge (Ganzzahl)
- Folgende Zeilen: Für jeden Vorgang der Typ (
deposit,withdrawoderbalance) und bei deposit/withdraw der Betrag in der nächsten Zeile
Beispiel für folgende Eingabe:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceDeine Ausgabe sollte folgendermaßen aussehen:
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30Das wichtigste Prinzip ist hier, dass die gesamte Synchronisierung innerhalb deiner BankAccount-Methoden verborgen ist. Aufrufer verwenden einfach Deposit(), Withdraw() und Balance(), ohne sich jemals Gedanken über Sperren machen zu müssen. Dein Struct kümmert sich intern um die Thread-Sicherheit.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Anfangssaldo lesen
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// Anzahl der Operationen lesen
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// Das Bankkonto erstellen
account := NewBankAccount(initial)
// Jede Operation verarbeiten
for i := 0; i < numOps; i++ {
opType, _ := reader.ReadString('\n')
opType = strings.TrimSpace(opType)
switch opType {
case "deposit":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Deposit aufrufen und das Ergebnis ausgeben
// Format: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Withdraw aufrufen und das entsprechende Ergebnis ausgeben
// Bei Erfolg: "Withdrew [amount], Balance: [new balance]"
// If failed: "Withdrawal failed: insufficient funds"
_ = amount // Diese Zeile entfernen, wenn du implementierst
case "balance":
// TODO: Balance aufrufen und das Ergebnis ausgeben
// Format: "Current balance: [balance]"
}
}
// TODO: Transaktionsverlauf ausgeben
// Der erste Eintrag sollte mit "History: " vorangestellt werden
// Nachfolgende Einträge sollten in neuen Zeilen ohne Präfix stehen
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker PoolÜbe selbstständig: Online-Go-Compiler