Go kennt kein class, kein extends, kein super. Stattdessen gibt es Embedding — ein Mechanismus, der einem Struct die Felder und Methoden eines anderen Typs „nach außen reicht", ohne dass eine Vererbungs-Hierarchie entsteht. Das Pattern ist idiomatische Composition: der äußere Typ hat einen inneren Typ als anonymes Feld, und der Compiler stellt die promoteten Member so dar, als wären sie direkt auf dem äußeren Typ deklariert. Dieser Artikel zeigt die Syntax, die Selector-Auflösung, die Method-Set-Regeln bei T vs. *T, Shadowing, das Diamond-Problem, Pointer-Embedding mit seiner Nil-Falle und zwei praxisnahe Patterns — Decorator und Logger — bei denen Embedding glänzt.
Warum kein klassisches extends?
Die meisten OOP-Sprachen lösen Code-Wiederverwendung über Vererbung: eine Klasse erbt Felder und Methoden von einer Basisklasse, kann sie überschreiben, kann super aufrufen. Das Modell ist mächtig, schleppt aber bekannte Probleme mit sich — fragile Basisklassen, tiefe Hierarchien, Diamond-Probleme bei Mehrfachvererbung, eine implizite Kopplung zwischen Sub- und Superklasse.
Go-Designer haben sich gegen klassische Vererbung entschieden und stattdessen auf das alte Prinzip „Composition over Inheritance" gesetzt. Die Sprache bietet einen Mechanismus für Code-Wiederverwendung zwischen Typen: das anonyme Feld, kurz Embedding genannt. Effective Go formuliert das pointiert:
Embedding types within a struct or interface lets you borrow pieces of an implementation. There is no subclassing in Go.
Heißt: ein eingebetteter Typ ist kein Elternteil. Er ist ein Bestandteil. Der äußere Typ besitzt ihn als Feld, nutzt seine Methoden und kann eigene Methoden hinzufügen oder gezielt überdecken. Es gibt kein super, keine Typ-Hierarchie, keine kovarianten Rückgaben. Was du bekommst, ist eine flache, explizite Komposition — und für jeden polymorphen Mechanismus, der über das hinausgeht, was Embedding leistet, sind Interfaces zuständig.
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Greet() string {
return "Hallo, ich bin " + a.Name
}
// Dog hat KEINE „Vererbung" von Animal — Dog enthält ein Animal.
// Greet wird trotzdem auf Dog aufrufbar, weil promoted.
type Dog struct {
Animal
Breed string
}
func main() {
d := Dog{Animal: Animal{Name: "Bella"}, Breed: "Border Collie"}
fmt.Println(d.Greet()) // promoted method
fmt.Println(d.Name, d.Breed) // promoted field + eigenes Feld
}Hallo, ich bin Bella
Bella Border CollieBeachte: d.Greet() sieht aus wie ein Methodenaufruf auf Dog, aber der Receiver ist intern d.Animal, also vom Typ Animal. Wenn Greet Felder ändern würde, würde sie das Animal-Feld in d ändern — nicht ein „geerbtes Selbst".
Syntax — anonymes vs. benanntes Feld
Die Go-Spec definiert ein eingebettetes Feld so:
A field declared with a type but no explicit field name is called an embedded field. An embedded field must be specified as a type name
Tor as a pointer to a non-interface type name*T, andTitself may not be a pointer type.
Der Typname selbst wird zum Feldnamen — ohne Paketpräfix. Das ist der einzige syntaktische Unterschied zu einem normalen Feld:
package main
import "sync"
// Variante A — benanntes Feld (klassische Composition).
type CacheA struct {
mu sync.Mutex // Feldname: mu
data map[string]string
}
// Variante B — anonymes Feld (Embedding).
type CacheB struct {
sync.Mutex // Feldname: Mutex (ohne sync-Präfix!)
data map[string]string
}
func main() {
var a CacheA
a.mu.Lock() // expliziter Zugriff über benanntes Feld
var b CacheB
b.Lock() // promoted — wirkt wie Methode auf CacheB
b.Mutex.Lock() // alternativ: expliziter Zugriff über Typname
}Drei Regeln aus der Spec, die du immer im Kopf haben solltest:
- Der unqualifizierte Typname ist der Feldname.
sync.Mutexwird zuMutex,pkg.FoozuFoo. Das ist auch die Antwort auf „Wie greife ich gezielt zu, wenn es Konflikte gibt?" — über den Typnamen. - Eingebettet werden darf ein Typname
Toder ein Pointer*T— aber kein zusammengesetzter Typ-Ausdruck.[]int,map[string]int,func()lassen sich nicht direkt embedden; sie brauchen einentype-Alias. - Innerhalb eines Structs müssen alle Feldnamen — inklusive der impliziten Namen aus Embeddings — eindeutig sein.
struct{ T; *T }ist illegal, weil der FeldnameTdoppelt vorkäme.
Eine häufige Verwechslung: type Inner struct { X int } direkt mit *Inner einzubetten ergibt einen Struct, der einen Pointer auf ein Inner enthält — nicht das Inner selbst. Die Methoden sind auch dann promoted, aber Zugriffe können nil-paniciren, wenn der Pointer nicht initialisiert wird. Dazu kommen wir gleich im Pointer-Embedding-Abschnitt.
Promoted Fields und Selector-Auflösung
Das Kern-Konzept: ein Feld oder eine Methode f des eingebetteten Typs ist promoted, wenn x.f ein legaler Selector am äußeren Typ ist. Die Spec sagt es so:
A field or method
fof an embedded field in a structxis called promoted ifx.fis a legal selector that denotes that field or methodf.
Was passiert mechanisch? Beim Selector-Lookup geht der Compiler den Pfad durch die eingebetteten Typen — Breadth-First, also Schicht für Schicht. Existiert auf der äußeren Schicht ein passender Name, gewinnt dieser. Sonst sucht er eine Schicht tiefer, bei allen Embeddings parallel. Findet er den Namen genau einmal, ist er auflösbar — findet er ihn mehrmals auf derselben Tiefe, ist der Selector mehrdeutig.
package main
import "fmt"
type Engine struct {
Horsepower int
}
func (e Engine) Start() string {
return fmt.Sprintf("Engine mit %d PS startet", e.Horsepower)
}
type Car struct {
Engine // Embedding
Brand string
}
func main() {
c := Car{
Engine: Engine{Horsepower: 150},
Brand: "Skoda",
}
// Promoted Field — Pfad: c.Engine.Horsepower
// Selector c.Horsepower ist legal, weil er eindeutig auflösbar ist.
fmt.Println(c.Horsepower) // 150
// Promoted Method — Receiver ist c.Engine, nicht c.
fmt.Println(c.Start())
// Expliziter Zugriff bleibt jederzeit erlaubt:
fmt.Println(c.Engine.Horsepower)
fmt.Println(c.Engine.Start())
}150
Engine mit 150 PS startet
Engine mit 150 PS startetWichtig zu verstehen: c.Horsepower ist syntaktischer Zucker für c.Engine.Horsepower. Es gibt im Speicher kein eigenes Horsepower-Feld auf Car; es existiert genau ein Horsepower in c.Engine. Wer das Feld liest oder schreibt, liest oder schreibt das innere Engine.
Eine wichtige Einschränkung: Composite Literals können promotete Felder nicht als Feldnamen verwenden. Wer ein Car baut, muss Engine: Engine{Horsepower: 150} schreiben — Horsepower: 150 direkt im Car-Literal ist ein Compile-Fehler. Die Begründung steht in der Spec:
Promoted fields act like ordinary fields of a struct except that they cannot be used as field names in composite literals of the struct.
Method Promotion und Method-Set-Regeln
Bei Methoden wird die Sache präziser, weil der Receiver-Typ (T vs. *T) ins Spiel kommt. Die Spec listet vier Fälle, und jede idiomatische Go-Codebasis lebt mit diesen Regeln:
| Embedded Feld | Method-Set von S enthält | Method-Set von *S enthält |
|---|---|---|
T | Methoden mit Receiver T | Methoden mit Receiver T und *T |
*T | Methoden mit Receiver T und *T | Methoden mit Receiver T und *T |
Lies das langsam: wer T einbettet (nicht den Pointer), bekommt am Wert S nur die Value-Receiver-Methoden seines inneren T. Wer dagegen *T einbettet, bekommt am Wert S schon beide Receiver-Varianten — weil ein Pointer ohnehin beide Method-Sets sieht.
package main
import "fmt"
type Inner struct{ N int }
func (i Inner) Read() int { return i.N } // Value-Receiver
func (i *Inner) Write(v int) { i.N = v } // Pointer-Receiver
type Outer struct {
Inner // Embedding: T (nicht *T)
}
func main() {
var o Outer
// Read ist Value-Receiver — am Wert und am Pointer aufrufbar.
_ = o.Read()
_ = (&o).Read()
// Write ist Pointer-Receiver. Am Wert nur dann aufrufbar,
// wenn o adressierbar ist (lokale Variable: ja).
o.Write(42) // Compiler fügt (&o.Inner) automatisch ein
fmt.Println(o.N)
// Im Interface-Kontext gilt strikt: nur *Outer erfüllt
// ein Interface, das Write erwartet — NICHT Outer.
}42Die Konsequenz für Interfaces ist entscheidend: Wer ein Interface mit Pointer-Receiver-Methoden über Embedding implementieren will, muss am Aufrufer-Ende einen Pointer haben. var w io.Writer = Outer{} funktioniert nicht, wenn der innere Typ Write als *Inner-Methode hat — var w io.Writer = &Outer{} funktioniert. Genauere Mechanik in Pointer vs. Wert.
Shadowing — Outer überdeckt Inner
Wenn der äußere Typ einen Member mit demselben Namen wie ein promoteter Member hat, gewinnt der äußere. Das ist die Shadowing-Regel, und sie ist die einzige Form von „Überschreiben", die Go kennt — bewusst ohne override-Keyword, ohne dynamische Dispatch-Magie:
package main
import "fmt"
type Logger struct{}
func (Logger) Log(msg string) {
fmt.Println("[base]", msg)
}
type AuditLogger struct {
Logger // Embedding
}
// Eigene Log-Methode am AuditLogger — überdeckt die promotete.
func (a AuditLogger) Log(msg string) {
fmt.Println("[audit]", msg)
a.Logger.Log(msg) // expliziter Aufruf der inneren Methode
}
func main() {
var a AuditLogger
a.Log("Datei gelöscht")
}[audit] Datei gelöscht
[base] Datei gelöschtZwei Aspekte, die immer wieder überraschen:
- Shadowing geschieht statisch zur Compile-Zeit, nicht dynamisch zur Laufzeit.
a.Log("...")löst beim Compiler den Selector auf, und der äußereAuditLogger.Logist näher als der innereLogger.Log. Es gibt keine virtuelle Tabelle, kein Polymorphismus — wer den innerenLogruft, ruft nicht automatisch den äußeren. Ein anderer Code, der direkta.Logger.Log("...")schreibt, bekommt die innere Variante. Das ist nicht wiesuper.method()in Java. - Die Tiefe zählt. Wenn
Outer { Middle }undMiddle { Inner }beide ein FeldFhaben, gewinnt dasFaufMiddlegegenüberInner— kürzerer Pfad schlägt längeren. Das gilt nur, wenn die Tiefen unterschiedlich sind; gleich tiefe Treffer sind mehrdeutig (siehe nächster Abschnitt).
Mehrfach-Embedding und das Diamond-Problem
Go erlaubt einem Struct, mehrere Typen gleichzeitig einzubetten. Damit kommt die klassische Diamond-Frage auf den Tisch: was passiert, wenn zwei eingebettete Typen einen Member mit demselben Namen exportieren?
Antwort: Solange du den Namen nicht benutzt, ist alles legal. Sobald du ihn als Selector verwendest, ist es ein Compile-Fehler wegen Ambiguität — es sei denn, einer der Treffer liegt näher (kürzerer Pfad).
package main
type Reader struct{}
func (Reader) Close() error { return nil }
type Writer struct{}
func (Writer) Close() error { return nil }
type ReadWriter struct {
Reader
Writer
}
func main() {
var rw ReadWriter
// Compile-Fehler: ambiguous selector rw.Close
// _ = rw.Close()
// Expliziter Pfad — kein Problem:
_ = rw.Reader.Close()
_ = rw.Writer.Close()
}Auflösungs-Strategien für Diamond-Konflikte:
- Expliziter Pfad:
rw.Reader.Close()oderrw.Writer.Close(). Macht klar, welcher der beiden Closes gemeint ist. - Outer-Methode definieren, die selbst entscheidet, was passiert:
func (rw *ReadWriter) Close() error { … }. Damit verschwindet die Ambiguität, weil der äußere Treffer den inneren überdeckt. - Strukturierung ändern: oft signalisiert ein Diamond-Konflikt, dass die Composition falsch geschnitten ist. Vielleicht sollte einer der beiden Typen nur als benanntes Feld eingebunden werden, kein Embedding.
Effective Go hebt einen wichtigen Schutz-Mechanismus hervor: Solange der Name nirgendwo verwendet wird, ist die Ambiguität kein Fehler. So bleiben Erweiterungen am inneren Typ rückwärtskompatibel — wenn Reader später ein Feld Name bekommt und Writer auch eines, bricht nichts, solange niemand rw.Name schreibt.
Pointer-Embedding und die Nil-Falle
Statt T lässt sich auch *T einbetten. Das ist häufig die richtige Wahl, wenn der innere Typ groß ist, wenn er nicht-kopierbar ist (Mutex!), oder wenn man optional einen inneren Wert haben will. Effective Go zeigt das anhand der bufio.ReadWriter:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// bufio-ähnliches Pattern: Pointer-Embedding, weil Reader/Writer
// selbst Pointer-Receiver-Methoden haben.
type ReadWriter struct {
*bufio.Reader
*bufio.Writer
}
func main() {
rw := &ReadWriter{
Reader: bufio.NewReader(strings.NewReader("Hallo Welt\n")),
Writer: bufio.NewWriter(os.Stdout),
}
line, _ := rw.ReadString('\n') // promoted vom *bufio.Reader
fmt.Print("Gelesen: ", line)
rw.WriteString("Geschrieben\n") // promoted vom *bufio.Writer
rw.Flush()
}Gelesen: Hallo Welt
GeschriebenDer entscheidende Unterschied zum Embedding via Wert: ein eingebetteter Pointer kann nil sein. Wer den äußeren Struct mit var rw ReadWriter deklariert, ohne die inneren Pointer zu setzen, kriegt beim ersten promoted-Methodenaufruf einen Nil-Pointer-Panic:
package main
import "bufio"
type Wrapper struct {
*bufio.Reader // Pointer-Embedding
}
func main() {
var w Wrapper // Reader bleibt nil
// w.ReadString('\n') würde paniken:
// runtime error: invalid memory address or nil pointer dereference
_ = w
}Faustregeln zum Pointer-Embedding:
- Konstruktor anbieten, der die inneren Pointer garantiert füllt:
func NewWrapper(r io.Reader) *Wrapper { return &Wrapper{Reader: bufio.NewReader(r)} }. - Zero-Value vermeiden, wenn der äußere Typ ohne den Pointer nicht funktioniert. Dokumentation des Typs sollte klar sagen „muss über
NewXgebaut werden". - Niemals einen Pointer-Embed nutzen, nur „weil es kürzer ist". Wenn der innere Typ klein und immutable ist, ist Wert-Embedding sicherer.
Interface-Embedding — kurz und klar
Embedding gibt es nicht nur in Structs, sondern auch in Interfaces. Effective Go zeigt das kanonische Beispiel:
package main
// Komposition über Interface-Embedding:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter erbt die Methoden-Signatur beider Interfaces.
type ReadWriter interface {
Reader
Writer
}Die Semantik ist sauber: ReadWriter ist die Vereinigung der Method-Sets — wer ReadWriter implementieren will, muss Read UND Write implementieren. Zwei Regeln aus der Spec, die du im Hinterkopf haben solltest:
- Nur Interfaces dürfen in Interfaces eingebettet werden (im klassischen Sinn). Mit den neuen Type-Sets ab Go 1.18 ist auch Konstruktion mit Typ-Constraints möglich, das ist aber ein eigenes Thema.
- Wenn zwei eingebettete Interfaces eine gleichnamige Methode mit unterschiedlicher Signatur deklarieren, ist das Interface ungültig. Bei identischer Signatur ist es legal, der Method-Eintrag erscheint einmal.
Im Unterschied zum Struct-Embedding gibt es bei Interfaces kein „Promotion auf Werte" — Interfaces beschreiben nur, welche Methoden ein Typ haben muss. Sie speichern keinen Zustand, sie haben keine Receiver-Set-Asymmetrien zwischen T und *T.
Praxis — Decorator-Pattern mit LoggingWriter
Embedding glänzt bei Decorator-Patterns: ein neuer Typ umhüllt einen bestehenden, gibt fast alles unverändert weiter und überschreibt punktuell eine einzelne Methode. Das klassische Beispiel ist ein io.Writer, der jedes Write zusätzlich loggt:
package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
)
// LoggingWriter umhüllt einen beliebigen io.Writer.
// Durch Embedding kriegt der äußere Typ ALLE Methoden des inneren —
// wir müssen nur Write überdecken, der Rest läuft automatisch.
type LoggingWriter struct {
io.Writer // anonymes Interface-Feld
Prefix string
logger *log.Logger
}
// Eigene Write-Methode überdeckt die promotete.
// Wir loggen, dann delegieren wir an den inneren Writer.
func (lw *LoggingWriter) Write(p []byte) (int, error) {
lw.logger.Printf("%s: %d bytes", lw.Prefix, len(p))
return lw.Writer.Write(p) // expliziter Aufruf am inneren Wert
}
func main() {
var buf bytes.Buffer
lw := &LoggingWriter{
Writer: &buf,
Prefix: "stdout",
logger: log.New(os.Stderr, "[trace] ", 0),
}
// lw erfüllt io.Writer dank der eigenen Write-Methode.
fmt.Fprintln(lw, "Hallo Welt")
fmt.Fprintln(lw, "Zweite Zeile")
fmt.Println("---buffer---")
fmt.Print(buf.String())
}[trace] stdout: 11 bytes
[trace] stdout: 13 bytes
---buffer---
Hallo Welt
Zweite ZeileWas hier idiomatisch ist:
- Das eingebettete Feld ist ein Interface (
io.Writer), kein konkreter Typ. Der Decorator funktioniert dadurch mit jedem Writer —os.Stdout,bytes.Buffer,net.Conn, Datei-Handles. Polymorphismus über Interface-Composition, ohne Vererbung. - Die
Write-Methode aufLoggingWriterüberdeckt die promotete und ruft explizitlw.Writer.Write(p)auf, um den inneren Writer zu erreichen. Wer das vergisst, baut eine Endlos-Rekursion — Klassik-Falle bei Decorator-Patterns. - Alle anderen Methoden, die der innere Writer ggf. zusätzlich hat (etwa
Close, wenn es einio.WriteCloserwäre), würden automatisch durchgereicht. Effective Go nennt das die „Avoidance of bookkeeping" — kein manuelles Forwarding nötig.
Praxis — sync.Mutex einbetten oder doch als Feld?
Ein historisch beliebtes Pattern: sync.Mutex als anonymes Feld einbetten, damit Lock() und Unlock() direkt am äußeren Typ aufrufbar sind. Das funktioniert technisch perfekt — der Mutex wird promoted, c.Lock() ruft c.Mutex.Lock():
package main
import "sync"
type Cache struct {
sync.Mutex // Embedding
data map[string]string
}
func (c *Cache) Set(k, v string) {
c.Lock() // promoted aus sync.Mutex
defer c.Unlock()
c.data[k] = v
}Inzwischen lautet die moderne Empfehlung der Go-Maintainer aber: Mutex nicht mehr einbetten, sondern als benanntes Feld halten — meist mu sync.Mutex. Die Gründe sind subtil, aber wichtig:
package main
import "sync"
type Cache struct {
mu sync.Mutex // benanntes Feld, vorzugsweise unexportiert
data map[string]string
}
func (c *Cache) Set(k, v string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[k] = v
}Was du gewinnst:
- API-Hygiene:
Lock()undUnlock()werden nicht Teil der öffentlichen API vonCache. Externe Aufrufer sollten Locking als Implementierungsdetail sehen, nicht als Feature des Typs. Mit Embedding sind die Methoden öffentlich — wer das Cache-Objekt hält, kann fremd-locken. - Klarheit:
c.mu.Lock()macht im Code explizit, welchen Mutex du nimmst. Bei zwei eingebetteten Mutex-haltigen Typen verschwindet diese Klarheit. - Konflikt-Schutz: Ein eingebetteter
sync.RWMutexund ein eingebettetersync.Mutexwürden beide eineLock-Methode promoten — Diamond-Konflikt. Mit benannten Feldern unmöglich.
Faustregel: Mutexe nicht einbetten, außer der Typ ist selbst ein Synchronisations-Primitiv (wie es z. B. bei manchen eigenen Lock-Implementierungen sinnvoll sein kann).
Wann du NICHT embedden solltest
Embedding ist ein scharfes Werkzeug. Vier Anti-Patterns, die in Code-Reviews regelmäßig auffallen:
-
API-Leakage: Du embeddest einen Typ aus einem internen Helfer-Paket, und plötzlich erscheinen all seine Methoden als Teil deiner öffentlichen API. Reviewer sehen
Cache.expireOldEntries()als öffentliche Methode, obwohl es ein Implementierungsdetail des inneren Caches ist. Lösung: als benanntes Feld halten und nur die gewünschten Methoden explizit als Wrapper anbieten. -
„Vererbung im Tarnmodus": Wer aus C++/Java kommt, missbraucht Embedding gerne, um eine Klassenhierarchie nachzubauen —
Tierals Basis,HundundKatzeals „Subklassen", überschriebene Methoden. Das funktioniert syntaktisch, aber konzeptionell baust du dir die Probleme zurück in den Code, die Go gerade vermeiden will. Frag dich: passt hier nicht ein Interface mit zwei unabhängigen Implementierungen besser? -
Embeddings mit Identitäts-Konflikt: Wenn der äußere Typ logisch kein
Innerist, sondern nur einenInnerbenutzt, gehört das als benanntes Feld in die Struktur. Embedding suggeriert „ist ein"; einOrderist keinLogger, auch wenn er einen braucht. -
Tiefe Embedding-Ketten:
AembeddetB,BembeddetC,CembeddetD. Selectora.someFieldläuft drei Ebenen tief — schwer zu lesen, schwer zu refaktorieren, mehrdeutig sobald irgendwo ein Namens-Konflikt entsteht. Idiomatisch sind 1-2 Ebenen, selten 3.
Häufige Stolperfallen
Embedding ist KEINE Vererbung.
Der äußere Typ ist nicht „Kind" des inneren — er enthält ihn als Feld. Es gibt keinen super-Aufruf, keinen Polymorphismus per Default, keine virtuelle Dispatch-Tabelle. Wer eine Methode überdeckt, ändert nichts am Verhalten der inneren Methode bei Aufrufen über den inneren Pfad. outer.Method() und outer.Inner.Method() sind statisch zur Compile-Zeit verschieden auflösbar.
Composite Literals können Promoted Fields nicht direkt nutzen.
Car{Horsepower: 150} ist ein Compile-Fehler, auch wenn Horsepower promotet ist. Du musst über den Typnamen gehen: Car{Engine: Engine{Horsepower: 150}}. Das ist die einzige Stelle, an der Embedding seine „Transparenz" verliert — beim Initialisieren wird der Pfad sichtbar.
Pointer-Embedding kann nil sein und paniken.
type Wrapper struct { *Inner } mit var w Wrapper lässt den inneren Pointer auf nil. Jeder Aufruf einer promoteten Methode panikt mit Nil-Pointer-Dereference. Lösung: Konstruktor (NewWrapper) bereitstellen, der den inneren Pointer garantiert füllt, oder Wert-Embedding nutzen, wenn der innere Typ klein ist.
Method-Set-Asymmetrie zwischen T und *T bei Embedding.
Wenn Outer ein Inner (Wert) embeddet und Inner Pointer-Receiver-Methoden hat, sind diese nur am *Outer-Wert promoted, nicht am Outer-Wert. Konsequenz: var o Outer; o.PointerMethod() funktioniert nur, wenn o adressierbar ist. Bei Map-Werten oder Funktions-Rückgaben ist es ein Compile-Fehler — siehe Pointer vs. Wert.
Diamond-Konflikte sind erst beim Zugriff Fehler.
Ein Struct mit zwei Embeddings, die denselben Methodennamen exportieren, ist legal. Erst der Selector s.Name ist mehrdeutig und produziert einen Compile-Fehler. Effective Go nennt das eine Schutzfunktion gegen Abwärts-Brüche: solange du den Namen nicht nutzt, brichst du dein API nicht, wenn der innere Typ ein Feld dazubekommt.
sync.Mutex embedden ist heute Anti-Pattern.
Moderne Go-Code-Bases halten Mutexe als unexportiertes Feld mu sync.Mutex. Embedding macht Lock und Unlock Teil der öffentlichen API — externe Aufrufer können das Locking missbrauchen, und go vet-Warnungen werden lauter, sobald irgendwo ein Wert-Receiver mit eingebettetem Mutex kombiniert wird.
Endlos-Rekursion in Decorator-Methoden.
Wer func (d *Decorator) Method() { d.Method() } schreibt, statt d.Inner.Method(), baut sich eine Endlos-Rekursion. Der promotete Selector wird durch die eigene Methode überdeckt, also ruft d.Method() sich selbst. Beim Override eines eingebetteten Methodenaufrufs immer explizit den inneren Pfad nehmen.
API-Leakage durch versehentliche Promotion.
Wer einen Typ aus einem internen Helfer-Paket embeddet, exportiert dessen gesamtes Method-Set über den äußeren Typ. Plötzlich ist ein Detail wie cache.gcMark() Teil der öffentlichen API von Repository. Wer öffentliche API kontrollieren will, muss benanntes Feld + explizite Wrapper-Methoden nehmen — Embedding ist nur dann richtig, wenn alle Methoden des inneren Typs Teil der äußeren API sein sollen.
Weiterführende Ressourcen
Externe Quellen
- Struct types — Go Language Specification
- Selectors — Go Language Specification
- Method sets — Go Language Specification
- Effective Go: Embedding
- Go FAQ: Why is there no type inheritance?