Embedding in einem Beispiel
Ein Feld mit Typ, aber ohne Namen ist ein eingebettetes Feld. Die Felder und Methoden des eingebetteten Typs sind direkt über den äußeren Typ erreichbar.
Ausgabe:
Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}
Der Name des eingebetteten Felds ist sein Typname, User. So sprichst du es in Literalen an (User: User{...}) und wenn du den inneren Wert selbst brauchst (a.User). Ein promotetes Feld lässt sich in einem Literal nicht über seinen kurzen Namen setzen: Admin{Name: "Ana"} scheitert mit unknown field Name in struct literal of type Admin.
Promotete Methoden erfüllen Interfaces
Die Promotion fügt die Methoden des eingebetteten Typs dem Method Set des äußeren Typs hinzu. Also erfüllt der äußere Typ jedes Interface, das der innere erfüllt.
Die Regeln für Method Sets richten sich nach dem eingebetteten Typ. Bettest du T ein, werden die Methoden mit Value Receiver von T sowohl zum äußeren Wert als auch zum äußeren Pointer befördert, die Methoden mit Pointer Receiver von *T aber nur zum äußeren Pointer. Bettest du *T ein, werden beide Sets zu beiden befördert. Im Zweifel bette den Pointer ein oder nutze den äußeren Typ über einen Pointer.
Einen Pointer einzubetten hat seinen Preis: Der Pointer kann nil sein. Service{} ohne Logger kompiliert, und svc.Log(...) löst dann eine Panic aus.
Embedding ist keine Vererbung
Embedding sieht aus wie Subklassenbildung, aber drei Dinge verhalten sich anders als in Java oder Python.
Der äußere Typ ist nicht der innere Typ. Ein Admin ist kein User. Eine Funktion, die einen User nimmt, akzeptiert kein Admin; übergib a.User.
Es gibt kein virtuelles Dispatching. Eine promotete Methode läuft auf dem eingebetteten Wert und weiß nichts vom äußeren Typ. Ruft sie eine andere Methode auf, ist es die Version ihres eigenen Typs, selbst wenn der äußere Typ eine gleichnamige definiert.
Ausgabe:
woof
The animal says ...
In einer klassenbasierten Sprache würde Speak „woof“ ausgeben. In Go ist d.Speak() die Kurzform für d.Animal.Speak(), und der Receiver dieses Aufrufs ist das Animal. Willst du Verhalten, das sich je nach Typ unterscheidet, nimm ein Interface: Übergib einen Sounder an den Code, der ihn braucht.
Der innere Typ weiß nicht, dass er eingebettet ist. Es gibt kein super. Um eine promotete Methode zu erweitern, definier auf dem äußeren Typ eine gleichnamige Methode und ruf die innere explizit auf:
func (a Admin) Greet() string {
return a.User.Greet() + " (admin)"
}
Namenskonflikte und Verschattung
Ein Feld oder eine Methode des äußeren Typs verschattet ein gleichnamiges promotetes Element, wie Dog.Sound oben. Der weniger tief liegende Name gewinnt.
Befördern zwei eingebettete Typen auf derselben Tiefe denselben Namen, wird dieser Name mehrdeutig. Das Programm kompiliert trotzdem, solange nichts den mehrdeutigen Namen benutzt:
Löse den Konflikt, indem du den vollen Pfad benutzt oder ID auf dem äußeren Typ definierst.
Interfaces einbetten
Interfaces können andere Interfaces einbetten. Die Standardbibliothek baut auf diese Weise größere Interfaces:
type ReadWriter interface {
Reader
Writer
}
Auch ein Struct kann ein Interface einbetten. Das Struct erfüllt dieses Interface dann über den Wert, den du im Feld speicherst, und du überschreibst nur die Methoden, die dich interessieren. Das ist das Decorator-Pattern mit fast keinem Code:
Das explizite c.Reader.Read(p) ist nötig. c.Read(p) innerhalb von Read würde sich endlos selbst aufrufen.
Ein Interface in einen Test-Fake einzubetten ist eine verbreitete Abkürzung: Bette das große Interface ein, implementier die eine Methode, die der Test braucht, und lass den Rest weg. Jede nicht implementierte Methode löst beim Aufruf eine Panic wegen Nil-Pointer-Dereferenzierung aus, und in einem Test willst du oft genau das.
Ein verbreiteter Praxisfall: sync.Mutex einbetten
type Stats struct {
sync.Mutex
hits map[string]int
}
func (s *Stats) Hit(page string) {
s.Lock()
defer s.Unlock()
s.hits[page]++
}
Das liest sich gut, exportiert aber auch Lock und Unlock als Teil der API von Stats, also kann jeder Aufrufer dein Struct sperren. Bei Typen, die außerhalb des Pakets genutzt werden, hält ein benanntes Feld (mu sync.Mutex) das Lock privat. Diese Abwägung gilt für jedes Embedding: Alles, was der eingebettete Typ exportiert, wird Teil der öffentlichen Oberfläche deines Typs.
Häufige Fehler
- Virtuelles Dispatching erwarten. Promotete Methoden rufen nie die Überschreibungen des äußeren Typs auf.
- Promotete Felder in einem Literal setzen. Nimm den Namen des eingebetteten Typs:
Admin{User: User{Name: "Ana"}}. - Eingebettete nil-Pointer. Ein eingebettetes
*Toder Interface muss gesetzt sein, bevor seine Methoden aufgerufen werden. - Versehentliche API-Oberfläche. Embedding exportiert alle exportierten Methoden des inneren Typs auf deinem Typ.
Häufig gestellte Fragen
Was ist Struct Embedding in Go?
Ein Feld, das nur mit einem Typ und ohne Namen deklariert wird: type Admin struct { User; Level int }. Felder und Methoden des eingebetteten User werden befördert, also funktionieren a.Name und a.Greet() direkt auf einem Admin. Der eingebettete Wert ist trotzdem ein normales Feld, erreichbar als a.User.
Hat Go Vererbung?
Nein. Go hat keine Klassen und keine Vererbung von Subtypen. Embedding ermöglicht Wiederverwendung durch Komposition: Der äußere Typ bekommt die Methoden des inneren, aber ein Admin ist kein User. Du kannst kein Admin übergeben, wo ein User erwartet wird, und promotete Methoden können nicht zurück in die Methoden des äußeren Typs rufen. Polymorphie kommt in Go von Interfaces.
Wie initialisiert man ein eingebettetes Struct in Go?
Im Composite Literal benennst du das eingebettete Feld mit seinem Typnamen: Admin{User: User{Name: "Ana"}, Level: 2}. Promotete Felder kannst du im Literal nicht direkt setzen: Admin{Name: "Ana"} scheitert mit unknown field Name in struct literal of type Admin.
Kann man in Go ein Interface in ein Struct einbetten?
Ja. Das Struct erfüllt das Interface dann über den eingebetteten Wert, und du kannst einzelne Methoden überschreiben. Das ist bei Decorators und Test-Fakes verbreitet. Ist das eingebettete Interface-Feld nil, löst der Aufruf einer nicht überschriebenen Methode eine Panic wegen Nil-Pointer-Dereferenzierung aus.