Menu

Golang Interface: implizite Implementierung, any, nil-Falle

Go-Interfaces werden implizit erfüllt: Jeder Typ mit den passenden Methoden implementiert sie. Kleine Interfaces wie io.Reader und fmt.Stringer, das leere Interface any, die Falle mit nil-Interfaces und die Regel: Interfaces annehmen, Structs zurückgeben.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Ein Interface ist eine Menge von Methoden

Ein Interface-Typ listet Methodensignaturen auf. Jeder Typ, der diese Methoden hat, erfüllt das Interface, ohne dass eine Deklaration das sagt.

Weder Rect noch Circle erwähnt Shape. Das ist implizite Implementierung, das prägende Merkmal von Go-Interfaces. Du kannst also in deinem Paket ein Interface definieren, das Typen aus anderen Paketen schon erfüllen, ohne deren Code anzufassen.

Kleine Interfaces aus der Standardbibliothek

Go-Code bevorzugt Interfaces mit ein oder zwei Methoden. Die wichtigsten:

InterfaceMethodeGenutzt von
fmt.StringerString() stringder Ausgabe von fmt
errorError() stringjeder Funktion, die fehlschlagen kann
io.ReaderRead(p []byte) (n int, err error)Dateien, Netzwerk, gzip, HTTP-Bodies
io.WriterWrite(p []byte) (n int, err error)Dateien, Puffer, Hashes, HTTP-Responses
sort.InterfaceLen, Less, Swapdem Paket sort
http.HandlerServeHTTP(w, r)net/http

Weil io.Reader eine einzige Methode hat, implementieren ihn Dutzende Typen, und jede Funktion, die einen io.Reader nimmt, funktioniert mit allen:

Das Go-Sprichwort lautet: Je größer das Interface, desto schwächer die Abstraktion. Größere Interfaces entstehen durch Kombination kleiner: io.ReadWriter ist Reader plus Writer, geschrieben, indem ein Interface in ein anderes eingebettet wird.

any: das leere Interface

interface{} hat keine Methoden, also erfüllt es jeder Typ. Go 1.18 hat any als Alias eingeführt; beide sind identisch.

Ein any-Wert kann alles enthalten, aber du kannst fast nichts damit tun, bis du den konkreten Typ mit einer Type Assertion oder einem Type Switch zurückholst. Nimm lieber ein echtes Interface oder Generics, wenn die Menge der Typen bekannt ist. any passt zu wirklich dynamischen Daten wie dekodiertem JSON unbekannter Form und zur Ausgabe.

Was ein Interface-Wert enthält

Ein Interface-Wert ist ein Paar: ein dynamischer Typ und ein dynamischer Wert. var s Shape = Rect{3, 4} speichert den Typ Rect und eine Kopie des Werts. Der Aufruf s.Area() sucht die Methode von Rect zur Laufzeit.

Ein Interface ist nur dann nil, wenn beide Teile leer sind. Diese Regel verursacht den verwirrendsten Bug in Go.

Die Falle mit nil-Interfaces

Ein nil-Pointer, der in einem Interface gespeichert ist, ergibt ein Interface, das nicht nil ist.

Ausgabe:

false
*main.MyError true
true

validate(true) gibt ein error-Interface mit dem Typ *MyError und dem Wert nil zurück. Das Interface hat einen Typ, ist also nicht gleich nil, und der Zweig if err != nil des Aufrufers läuft. Ein Aufruf von err.Error() dort würde dann beim Feldzugriff über den nil-Receiver eine Panic auslösen.

Die Lösung ist einfach: Deklarier die Variable als error, nicht als konkreten Pointer-Typ, oder gib auf dem Erfolgspfad ein wörtliches nil zurück. Gib aus einer Funktion, deren Ergebnis error ist, nie einen konkreten Fehler-Pointer-Typ zurück. Dieselbe Falle gilt für jedes Interface, nicht nur für Fehler.

Prüfen, ob ein Typ ein Interface implementiert

Die Implementierung wird dort geprüft, wo ein Wert einem Interface zugewiesen wird. Tut das noch kein Code, bleibt ein Fehler in einer Methodensignatur unbemerkt. Eine Zuweisung an den Blank Identifier auf Paketebene macht die Prüfung explizit:

var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)

Das kostet zur Laufzeit nichts. Hat *LogWriter die Methode Write(p []byte) error statt Write(p []byte) (int, error), schlägt der Build fehl:

cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
		have Write([]byte) error
		want Write([]byte) (int, error)

Pointer Receiver und Interfaces

Hat eine Methode einen Pointer Receiver, besitzt nur der Pointer-Typ diese Methode. *Counter erfüllt darüber ein Interface, Counter nicht. Der Compiler meldet Counter does not implement Incrementer (method Inc has pointer receiver). Speichere &Counter{} im Interface. Die Seite zu Methoden erklärt Method Sets.

Interfaces annehmen, Structs zurückgeben

Eine verbreitete Go-Richtlinie: Funktionen sollten Interface-Parameter nehmen und konkrete Typen zurückgeben.

  • Ein Interface annehmen erlaubt Aufrufern, alles zu übergeben, was passt, auch Test-Fakes. Eine Funktion, die Daten liest, sollte einen io.Reader nehmen, keine *os.File.
  • Einen konkreten Typ zurückgeben erlaubt Aufrufern, alle seine Methoden und Felder zu nutzen, und vermeidet die nil-Interface-Falle. os.Open gibt *os.File zurück, nicht io.Reader.

Eine verwandte Gewohnheit: Definier Interfaces dort, wo sie genutzt werden, nicht dort, wo sie implementiert werden. Braucht dein Service etwas, das einen Nutzer per Get(id) holen kann, deklarier ein Interface mit einer Methode im Paket deines Service und lass das Datenbankpaket einfach sein Struct exportieren.

Interface-Werte vergleichen

Zwei Interface-Werte sind gleich, wenn ihre dynamischen Typen identisch und ihre dynamischen Werte gleich sind. Ist der dynamische Typ nicht vergleichbar (ein Slice, eine Map), kompiliert ==, löst aber zur Laufzeit eine Panic aus: runtime error: comparing uncomparable type []int.

Häufige Fehler

  • Einen typisierten nil-Pointer als Interface zurückgeben. Gib ein wörtliches nil zurück.
  • Interfaces zu früh. Schreib zuerst den konkreten Typ. Füg ein Interface hinzu, wenn eine zweite Implementierung oder ein Test eins braucht.
  • Pointer auf ein Interface. *io.Reader ist fast nie richtig. Ein Interface enthält schon einen Pointer, wenn du einen darin speicherst.
  • Große Interfaces. Interfaces mit zehn Methoden sind schwer zu implementieren und schwer zu faken. Teil sie auf.

Häufig gestellte Fragen

Wie implementiert man ein Interface in Go?

Definier auf deinem Typ die Methoden, die das Interface auflistet, mit denselben Namen und Signaturen. Ein Schlüsselwort implements gibt es nicht. Hat *File die Methode Read(p []byte) (int, error), ist es automatisch ein io.Reader. Der Compiler prüft das überall, wo du den Wert dem Interface-Typ zuweist.

Was ist das leere Interface oder any in Go?

interface{} hat keine Methoden, also erfüllt es jeder Typ. Seit Go 1.18 ist any ein eingebauter Alias für interface{}. Ein Wert vom Typ any kann alles enthalten, aber um wieder an einen konkreten Typ zu kommen, brauchst du eine Type Assertion oder einen Type Switch.

Warum ist mein Go-Interface nicht nil, obwohl ich einen nil-Pointer zugewiesen habe?

Ein Interface-Wert enthält einen Typ und einen Wert. Weist du einen nil-*MyError einem error zu, bekommst du ein Interface, dessen Typ *MyError und dessen Wert nil ist, und dieses Interface ist nicht gleich nil. Gib ein wörtliches nil statt eines typisierten nil-Pointers zurück, wenn es keinen Fehler gibt.

Wie prüfe ich zur Compilezeit, ob ein Typ ein Interface implementiert?

Füg auf Paketebene eine Zuweisung an den Blank Identifier hinzu: var _ io.Reader = (*MyReader)(nil). Fehlt *MyReader eine Methode, schlägt der Build mit einer Meldung fehl, die die fehlende Methode nennt.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S