Singleton-Muster
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 86 von 107.
Das Singleton-Muster stellt sicher, dass ein Typ während des gesamten Programms nur eine Instanz besitzt, und bietet einen globalen Zugriffspunkt darauf. Dies ist nützlich für gemeinsam genutzte Ressourcen wie Konfigurationsmanager, Datenbankverbindungen oder Logger.
In Go implementieren wir Singleton mithilfe einer Paketvariablen kombiniert mit sync.Once, um eine threadsichere Initialisierung zu gewährleisten:
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}sync.Once garantiert, dass die Initialisierungsfunktion genau einmal ausgeführt wird, selbst wenn mehrere Goroutinen gleichzeitig GetInstance() aufrufen. Jeder nachfolgende Aufruf gibt dieselbe Instanz zurück, ohne den Initialisierungscode erneut auszuführen.
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 und cfg2 zeigen auf dieselbe InstanzVerwende Singleton sparsam. Obwohl es praktisch ist, führt es einen globalen Zustand ein, der das Testen erschweren und Abhängigkeiten verbergen kann. Überlege, ob Dependency Injection für deinen Anwendungsfall eine bessere Alternative sein könnte.
Aufgabe
EinfachErstellen wir einen Singleton-Logger, der sicherstellt, dass in deiner gesamten Anwendung nur eine Logger-Instanz existiert! Dies ist ein klassischer Anwendungsfall für das Singleton-Muster. Alle Teile deines Programms sollen dieselbe Logger-Konfiguration und denselben Zustand verwenden.
Du organisierst deinen Code auf zwei Dateien verteilt:
logger.go: Implementiere deinen threadsicheren Singleton-Logger.Erstelle eine
Logger-Struktur mit zwei Feldern:Prefix(string) für Präfixe von Log-Nachrichten undMessageCount(int), um zu verfolgen, wie viele Nachrichten protokolliert wurden.Verwende Paketvariablen zusammen mit
sync.Once, um sicherzustellen, dass nur eine Instanz erstellt wird. Implementiere eineGetLogger()-Funktion, die den Logger mit dem Standardpräfix"[LOG]"initialisiert und die Singleton-Instanz zurückgibt.Füge deiner Logger-Struktur die folgenden Methoden hinzu:
SetPrefix(prefix string)– ändert das Präfix des LoggersLog(message string) string– erhöht die Anzahl der Nachrichten und gibt eine formatierte Zeichenfolge zurück:[prefix] #[count]: [message]GetCount() int– gibt die Gesamtzahl der protokollierten Nachrichten zurück
main.go: Zeige, dass mehrere Aufrufe vonGetLogger()dieselbe Instanz zurückgeben.Lies einen neuen Präfixwert ein und anschließend eine Anzahl von Nachrichten, gefolgt von jeder zu protokollierenden Nachricht.
Rufe die Logger-Instanz ab, setze das benutzerdefinierte Präfix, protokolliere anschließend jede Nachricht und gib das Ergebnis aus. Nachdem alle Nachrichten protokolliert wurden, rufe die Logger-Instanz erneut ab (um zu beweisen, dass es dieselbe ist) und gib die Gesamtzahl der Nachrichten aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Zu setzendes benutzerdefiniertes Präfix
- Zeile 2: Anzahl der Nachrichten
- Folgende Zeilen: Jede zu protokollierende Nachricht
Beispielsweise bei:
[APP]
3
Server started
User connected
Request processedsollte deine Ausgabe folgendermaßen aussehen:
[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3Und bei:
[DEBUG]
2
Initializing cache
Cache readysollte deine Ausgabe folgendermaßen aussehen:
[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2Und bei:
[ERROR]
1
Connection failedsollte deine Ausgabe folgendermaßen aussehen:
[ERROR] #1: Connection failed
Total messages logged: 1Die wichtigste Erkenntnis ist, dass du unabhängig davon, wie oft du GetLogger() aufrufst, immer dieselbe Instanz mit ihrem gemeinsamen Zustand erhältst. Die Anzahl der Nachrichten bleibt erhalten, weil nur ein einziger Logger existiert!
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Lese das benutzerdefinierte Präfix
prefix, _ := reader.ReadString('\n')
prefix = strings.TrimSpace(prefix)
// Lese die Anzahl der Nachrichten
countStr, _ := reader.ReadString('\n')
countStr = strings.TrimSpace(countStr)
count, _ := strconv.Atoi(countStr)
// Lese jede Nachricht
messages := make([]string, count)
for i := 0; i < count; i++ {
msg, _ := reader.ReadString('\n')
messages[i] = strings.TrimSpace(msg)
}
// TODO: Hole die Logger-Instanz mit GetLogger()
// TODO: Setze das benutzerdefinierte Präfix mit SetPrefix()
// TODO: Protokolliere jede Nachricht und gib das Ergebnis aus
// TODO: Hole die Logger-Instanz erneut (um zu beweisen, dass es dieselbe ist)
// and print the total message count in format: "Total messages logged: X"
}
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 Sammlung13Entwurfsmuster Teil 1
Einführung in EntwurfsmusterSingleton-MusterFactory-MusterAbstraktes Factory-MusterObserver-MusterStrategy-Muster2Typen & 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