Regeln zur Interface-Erfüllung
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 43 von 107.
Während Go Duck-Typing für die implizite Erfüllung von Schnittstellen verwendet, gibt es bestimmte Regeln, die bestimmen, ob ein Typ tatsächlich eine Schnittstelle erfüllt. Wenn du diese Regeln verstehst, kannst du subtile Fehler vermeiden.
Die wichtigste Regel betrifft Pointer-Receiver. Wenn eine Methode mit einem Pointer-Receiver definiert ist, erfüllt nur ein Pointer auf diesen Typ das Interface, nicht der Wert selbst:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // Zeigerempfänger
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
Da Save() einen Pointer-Empfänger hat, erfüllt nur *Document die Anforderungen von Saver:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // funktioniert - Zeiger erfüllt Interface
// Process(doc) // Kompilierfehler - Wert erfüllt Interface nicht
}
Die umgekehrte Richtung ist jedoch flexibler. Wenn eine Methode einen Wertempfänger hat, erfüllen sowohl der Werttyp als auch der Zeigertyp das Interface. Go dereferenziert Zeiger automatisch, wenn Methoden mit Wertempfänger aufgerufen werden:
func (d Document) Info() string { // Wert-Receiver
return d.Name
}
// Sowohl Document als auch *Document erfüllen ein Interface, das Info() erfordert
Diese Asymmetrie besteht, weil Go immer einen Wert von einem Zeiger erhalten kann (durch Dereferenzierung), aber nicht immer einen Zeiger aus einem Wert erhalten kann (der Wert ist möglicherweise nicht adressierbar). Wenn du diese Regel im Hinterkopf behältst, verhinderst du verwirrende Compilerfehler bei der Arbeit mit Interfaces.
Aufgabe
EinfachErstellen wir ein Konfigurationssystem, das zeigt, wie sich pointer- und value-Receiver auf die Erfüllung eines interface auswirken. Du erstellst Typen, bei denen die Wahl des Receivers bestimmt, ob Werte, pointer oder beides mit einem interface verwendet werden können.
Du organisierst deinen Code über drei Dateien:
config.go: Define einConfigurable-interface, das zwei methods erfordert:GetValue() stringundSetValue(string). Erstelle anschließend zwei Konfigurationstypen:ReadOnlyConfigmit einemValue-field: Verwende einen value-Receiver fürGetValue()(gibt den Value zurück) und einen pointer-Receiver fürSetValue()(aktualisiert den Value).Settingmit einemData-field: Verwende value-Receiver für both methods (GetValuegibt Data zurück,SetValuegibt einfach "Cannot modify" aus, ohne etwas zu ändern).
processor.go: Erstelle eine function namensProcessConfig, die einConfigurableund einen neuen value-String accepts. Sie soll den current value mitGetValue()ausgeben,SetValue()mit dem neuen value aufrufen und anschließend den value erneut ausgeben, um any changes zu zeigen.main.go: Lies Konfigurationsdetails aus der Eingabe und veranschauliche die Regeln für die Erfüllung des interface:- Erstelle ein
ReadOnlyConfigund übergib einen pointer anProcessConfig(erforderlich, becauseSetValueeinen pointer-Receiver hat). - Erstelle ein
Settingund übergib den value direkt anProcessConfig(funktioniert, because both methods value-Receiver haben).
- Erstelle ein
Die folgenden Eingaben werden bereitgestellt:
- Line 1: Initialer value für ReadOnlyConfig
- Line 2: Neuer value, der für ReadOnlyConfig gesetzt werden soll
- Line 3: Initialer value für Setting
- Line 4: Neuer value, der für Setting versucht werden soll
Deine ProcessConfig-function sollte in diesem format ausgeben:
Current: [value]
Current: [value after SetValue]Wenn beispielsweise debug, production, localhost und remote angegeben werden, sollte deine Ausgabe folgendermaßen aussehen:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostBeachte, wie ReadOnlyConfig seinen value tatsächlich ändert (because wir einen pointer übergeben haben), während Setting unverändert bleibt (sein SetValue mit einem value-Receiver kann den ursprünglichen value nicht modify). Die wichtigste Erkenntnis ist, dass ReadOnlyConfig-Werte allein Configurable nicht erfüllen würden, sondern nur pointer, während Setting-Werte direkt funktionieren, because alle methods von Setting value-Receiver verwenden.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Eingaben lesen
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: Erstelle ein ReadOnlyConfig mit dem Wert rocInitial
// TODO: Übergebe einen POINTER an ProcessConfig (erforderlich, weil SetValue einen Pointer-Receiver hat)
// TODO: Erstelle ein Setting mit dem Wert settingInitial
// TODO: Übergebe den VALUE direkt an ProcessConfig (funktioniert, weil beide Methoden Value-Receiver haben)
// Verwende diese Variablen, um Fehler durch ungenutzte Variablen zu vermeiden
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
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