Entwurfsmuster in Go: Keine „Religion“, sondern ein Werkzeug
\n\nAls ich anfing, in Go zu programmieren, verfolgte mich das Gefühl, dass all diese „Gang of Four“ und ihre 23 Muster etwas aus der Welt von Java und C++ seien. Go ist doch so einfach und prägnant – warum brauchen wir diese Komplexität? Aber je tiefer ich in reale Projekte eintauchte (Microservices, CLI-Tools, API-Handler), desto öfter ertappte ich mich bei dem Gedanken: „Hier wäre eine Factory-Methode nützlich“ oder „Diesen Berg von if-else könnte man durch eine Strategie ersetzen.“
\n\nEntwurfsmuster in Go sind kein Dogma und kein Versuch, OOP auf eine funktionale Sprache zu übertragen. Es sind bewährte Lösungen für typische Probleme. Sie helfen dabei, Code testbar, flexibel und für Kollegen verständlich zu machen. Und ja, Go hinterlässt seine Spuren: Aufgrund des Fehlens klassischer Vererbung und Generics (vor Version 1.18) werden einige Muster transformiert, andere werden überflüssig.
\n\nIn diesem Artikel werden wir nicht alle 23 Muster auflisten. Stattdessen analysieren wir die 5 gefragtesten Vorlagen in Golang, die in echten Projekten wirklich nützlich sind. Sie werden sehen, wann man sie anwenden sollte und wann man besser die Finger davon lässt.
\n\nSingleton-Muster in Go: Nicht nur ein Singleton, sondern ein Singleton auf Go-Art
\n\nSingleton stellt sicher, dass es von einer Struktur nur eine Instanz gibt. In Go wird dies oft für Konfigurationen, Verbindungspools oder Logger verwendet. Es gibt jedoch eine Nuance: In Go gibt es keine statischen Klassen, dafür aber sync.Once – das ideale Werkzeug für faule Initialisierung.
Problem ohne Singleton
\nStellen Sie sich vor, Sie haben eine Struktur Config, die Einstellungen aus einer Datei liest. Wenn jedes Paket seine eigene Instanz erstellt, lesen Sie die Datei entweder 10 Mal (langsam) oder erhalten eine Race-Condition.
Lösung mit Singleton
\npackage config\n\nimport (\n "sync"\n "encoding/json"\n "os"\n)\n\ntype Config struct {\n Port int `json:"port"`\n Env string `json:"env"`\n}\n\nvar (\n instance *Config\n once sync.Once\n)\n\nfunc GetConfig() *Config {\n once.Do(func() {\n file, _ := os.Open("config.json")\n defer file.Close()\n instance = &Config{}\n json.NewDecoder(file).Decode(instance)\n })\n return instance\n}\n\nWann verwenden: wenn eine garantierte Einzigartigkeit der Instanz erforderlich ist (Konfigurationen, Caches, Fabriken).
\nWann nicht: für alles Mögliche. Ein Singleton ist ein globaler Zustand, der das Testen erschwert. Wenn Sie eine Abhängigkeit über den Konstruktor übergeben können – tun Sie das.
\n\nFactory-Muster: Wenn die Objekterstellung komplex wird
\n\nFactory Method und Abstract Factory dienen der Kapselung der Logik zur Objekterstellung. In Go ist dies besonders relevant, da wir keine Konstruktoren im klassischen Sinne haben. Stattdessen verwenden wir Konstruktorfunktionen mit dem Präfix New.
Beispiel: Erstellung von Benachrichtigungen
\nAngenommen, Ihr System sendet Benachrichtigungen per E-Mail, SMS und Push. Anstatt die Auswahllogik über den Code zu verteilen, verwenden wir eine Factory.
\n\npackage notification\n\nimport "fmt"\n\ntype Notifier interface {\n Send(message string) error\n}\n\ntype EmailNotifier struct{}\n\nfunc (e EmailNotifier) Send(message string) error {\n fmt.Println("Sending email:", message)\n return nil\n}\n\ntype SMSNotifier struct{}\n\nfunc (s SMSNotifier) Send(message string) error {\n fmt.Println("Sending SMS:", message)\n return nil\n}\n\nfunc NewNotifier(typ string) Notifier {\n switch typ {\n case "email":\n return EmailNotifier{}\n case "sms":\n return SMSNotifier{}\n default:\n return nil\n }\n}\n\nWann verwenden: wenn die Erstellungslogik komplex ist (abhängig von Typ, Version, Konfiguration).
\nWann nicht: wenn Sie nur einen Objekttyp haben. Schreiben Sie einfach MyStruct{} oder NewMyStruct() ohne Factory.