Interface Satisfaction ist die zentrale Frage hinter jedem Interface in Go: Wann genau gilt ein Typ als Implementierung eines Interfaces? Die Antwort ist formal, knapp und folgenreich — die Method-Set-Regel der Spec. Sie entscheidet darüber, ob var w io.Writer = myBuf kompiliert oder mit der berüchtigten Fehlermeldung „does not implement" abbricht. Sie erklärt, warum *T mehr Interfaces erfüllen kann als T, warum bytes.Buffer{} als Wert reicht, aber nur über die Adresse als io.Writer durchgeht, und warum jedes ernsthafte Go-Package ein winziges Compile-Time-Check-Idiom enthalten sollte. Dieser Artikel zerlegt die Regel in ihre Bestandteile, zeigt die Asymmetrie zwischen T und *T, das var _ I = (*T)(nil)-Idiom und die typischen Stolpersteine — adressierbar vs. nicht, Wert-Receiver-Mismatch, Embedding-Propagation.

Die Spec-Regel — Method Set als Obermenge

Bevor wir Beispiele sehen, brauchst du die formale Regel im Kopf. Die Go-Spec definiert Implementing an interface in einem Satz:

A type T implements an interface I if T is not an interface and is an element of the type set of I; or T is an interface and the type set of T is a subset of the type set of I.

Für gewöhnliche Method-Interfaces — also alles ohne Type-Constraints aus Generics — reduziert sich der erste Fall auf eine einfache, praxistaugliche Regel: Ein Typ T erfüllt das Interface I, wenn das Method Set von T eine Obermenge des Method Sets von I ist. Jede Methode, die I verlangt, muss in T mit passender Signatur vorhanden sein. Mehr Methoden sind erlaubt, fehlende nicht.

Das Method Set selbst definiert die Spec im Abschnitt Method sets:

The method set of a defined type T consists of all methods declared with receiver type T. The method set of a pointer to a defined type T (where T is neither a pointer nor an interface) is the set of all methods declared with receiver *T or T.

Zwei Stichworte, drei Konsequenzen — und genau hier kommt die Asymmetrie ins Spiel, an der die meisten Anfänger zum ersten Mal scheitern. Das *T-Method-Set ist immer eine Obermenge des T-Method-Sets. Nie umgekehrt.

Method Set — kurze Wiederholung

Das Method Set ist die Menge aller Methoden, die du auf einem Wert dieses Typs aufrufen kannst. Die volle Behandlung mit Receiver-Wahl steht im Artikel Value vs. Pointer Receiver — hier nur das Nötigste für die Interface-Frage.

Go method-set-recap.go
package main

type Buffer struct {
    data []byte
}

// Value-Receiver — Methode ist Teil von Buffer UND *Buffer.
func (b Buffer) Len() int {
    return len(b.data)
}

// Pointer-Receiver — Methode ist NUR Teil von *Buffer.
func (b *Buffer) Write(p []byte) (int, error) {
    b.data = append(b.data, p...)
    return len(p), nil
}

// Method Set von Buffer  : { Len }
// Method Set von *Buffer : { Len, Write }

Lies das Kommentar zweimal: Buffer hat eine Methode, *Buffer hat zwei. Write mit Pointer-Receiver wandert nicht „nach unten" in das Method Set von Buffer — die Spec ist hier strikt asymmetrisch.

T vs. *T — die Asymmetrie als Bild

Stell dir Method Sets als zwei Kreise vor. Der äußere Kreis ist *T, der innere ist T. Jede Value-Receiver-Methode liegt in beiden Kreisen. Jede Pointer-Receiver-Methode liegt nur im äußeren. Welches Interface ein Wert erfüllt, hängt davon ab, in welchem Kreis du dich befindest.

Go t-vs-pt.go
package main

import "fmt"

type Greeter interface {
    Greet() string
}

type Mutator interface {
    SetName(string)
}

type Person struct {
    name string
}

// Value-Receiver — in Person UND *Person.
func (p Person) Greet() string {
    return "Hallo, " + p.name
}

// Pointer-Receiver — NUR in *Person.
func (p *Person) SetName(n string) {
    p.name = n
}

func main() {
    p := Person{name: "Alice"}

    // Greeter verlangt Greet() — beide Typen erfüllen das.
    var g1 Greeter = p   // ok
    var g2 Greeter = &p  // ok
    fmt.Println(g1.Greet(), "|", g2.Greet())

    // Mutator verlangt SetName(string) — nur *Person erfüllt das.
    // var m1 Mutator = p  // COMPILE ERROR
    var m2 Mutator = &p     // ok
    m2.SetName("Bob")
    fmt.Println(p.Greet())
}
Output
Hallo, Alice | Hallo, Alice
Hallo, Bob

Die auskommentierte Zeile var m1 Mutator = p ist der klassische Anfänger-Fehler. Der Compiler bricht ab mit „method SetName has pointer receiver". Lösung ist immer dieselbe — nimm die Adresse.

Adressierbarkeit — Methodenaufruf vs. Interface-Zuweisung

Hier wird es subtil, und genau hier verlieren viele die Spur. Beim direkten Methodenaufruf fügt Go den Adress-Operator & automatisch ein, wenn der Wert adressierbar ist. Bei der Interface-Zuweisung macht es das nicht. Das Method Set bleibt unverändert — und damit auch die Frage, ob das Interface erfüllt ist.

Go addressable-vs-interface.go
package main

import "fmt"

type Counter struct {
    n int
}

func (c *Counter) Inc() {
    c.n++
}

type Incrementer interface {
    Inc()
}

func main() {
    c := Counter{}

    // Direkter Methodenaufruf — Go fügt &c automatisch ein,
    // weil c adressierbar ist (lokale Variable).
    c.Inc()
    fmt.Println("nach c.Inc():", c.n) // 1

    // Interface-Zuweisung — keine Auto-&-Einfügung.
    // Method Set von Counter enthält Inc NICHT.
    // var i Incrementer = c  // COMPILE ERROR

    // Mit explizitem & geht es:
    var i Incrementer = &c
    i.Inc()
    fmt.Println("nach i.Inc():", c.n) // 2
}
Output
nach c.Inc(): 1
nach i.Inc(): 2

Warum die Unterscheidung? Die Auto-Adressierung beim Methodenaufruf ist eine Komfort-Regel der Sprache, kein Eingriff in das Method Set. Bei der Interface-Zuweisung würde Auto-Adressierung still einen Zeiger auf einen temporären Wert erzeugen — und Mutation über das Interface würde an einer Kopie verpuffen. Das wäre eine Falle, also macht Go es nicht.

Die Komfort-Regel funktioniert nicht überall — selbst beim direkten Aufruf gibt es Stellen, an denen c.Inc() scheitert:

Go not-addressable.go
package main

type Counter struct{ n int }

func (c *Counter) Inc() { c.n++ }

func makeCounter() Counter {
    return Counter{}
}

func main() {
    m := map[string]Counter{"a": {}}

    // makeCounter().Inc()  // FEHLER — Return-Wert nicht adressierbar
    // m["a"].Inc()         // FEHLER — Map-Element nicht adressierbar

    // Wert in einer Variable: ist adressierbar, geht.
    c := makeCounter()
    c.Inc()
    _ = c
}

Return-Werte und Map-Elemente sind nicht adressierbar. Wer dort eine Pointer-Receiver-Methode aufrufen will, muss erst in eine lokale Variable umkopieren. Mehr Details im Adresse-und-Dereferenzierung-Artikel.

Wann ist *T Pflicht für ein Interface?

Die Regel folgt direkt aus dem Method Set: Sobald eine vom Interface verlangte Methode mit Pointer-Receiver deklariert ist, erfüllt nur *T das Interface — T allein nicht. Das gilt selbst dann, wenn alle anderen Methoden Value-Receiver haben.

Go pointer-pflicht.go
package main

import "fmt"

type Storer interface {
    Load() string
    Save(string)
}

type FileStore struct {
    content string
}

func (f FileStore) Load() string  { return f.content }   // value
func (f *FileStore) Save(s string) { f.content = s }     // pointer

func main() {
    f := FileStore{content: "initial"}

    // var s Storer = f   // FEHLER — Save hat Pointer-Receiver
    var s Storer = &f     // ok
    s.Save("updated")
    fmt.Println(s.Load())
}
Output
updated

Praktische Konsequenz für das Design eigener Typen: Wenn ein Typ ein Interface implementieren soll, das Mutation verlangt, müssen die Methoden Pointer-Receiver haben, und der Aufrufer arbeitet mit *T. Das ist die Standard-Form für alles, was wir in der Stdlib unter „Implementierungen von Interfaces" sehen — *bytes.Buffer ist io.Writer, *os.File ist io.Reader, *http.ServeMux ist http.Handler.

Das Compile-Time-Check-Idiom

Ein Interface in Go wird implizit erfüllt — es gibt kein implements-Schlüsselwort. Das ist mächtig, aber auch riskant: Wenn du in einer Methode versehentlich die Signatur falsch tippst (Write(p []byte) error statt Write(p []byte) (int, error)), implementiert dein Typ das Interface plötzlich nicht mehr — und du merkst es erst, wenn jemand versucht, ihn als io.Writer zu nutzen. Im schlimmsten Fall passiert das in einem anderen Package, das erst zur Laufzeit instanziiert wird.

Das idiomatische Gegenmittel ist eine einzige Zeile am Anfang oder Ende des Packages:

Go compile-check-idiom.go
package storage

import "io"

// Compile-Time-Check: Wenn FileStore io.Writer nicht erfüllt,
// bricht der Build genau hier ab — nicht irgendwo später.
var _ io.Writer = (*FileStore)(nil)

type FileStore struct {
    path string
}

func (f *FileStore) Write(p []byte) (int, error) {
    // ...
    return len(p), nil
}

Die Zerlegung der Zeile:

  • var _ — Deklaration einer Variable mit Blank Identifier. Sie wird nie gelesen, nur typgeprüft.
  • io.Writer — der Ziel-Typ. Hier muss das Interface stehen, das du behaupten willst.
  • (*FileStore)(nil) — ein typisierter nil-Wert vom Typ *FileStore. Kein Speicher wird allokiert, kein Konstruktor läuft.

Die Zuweisung selbst zwingt den Compiler, das Method Set von *FileStore gegen io.Writer zu prüfen. Stimmt es nicht, bricht der Build sofort ab — bevor irgendein Test, irgendein anderes Package, irgendein Linter Zeit hat, es anders zu kommentieren. Die Fehlermeldung zeigt direkt auf die Compile-Check-Zeile, nicht auf einen weit entfernten Aufrufer.

Wenn dein Typ als Wert ein Interface erfüllen soll, verwendest du FileStore{} statt (*FileStore)(nil):

Go value-compile-check.go
var _ fmt.Stringer = Money{} // Wert-Variante für Value-Receiver-Typen

Warum dieses Idiom in jedem Package stehen sollte

Drei Gründe, das Compile-Check-Idiom zur Hausregel zu machen:

  • Frühe Fehler. Eine falsche Signatur fällt beim Editor-Save auf, nicht beim ersten Integrationstest. Der Pfad zum Fehler ist eine Zeile, kein Stack-Trace.
  • Dokumentation für Leser. Ein var _ http.Handler = (*Router)(nil) am Anfang einer Datei kommuniziert sofort: Dieser Typ ist als http.Handler gedacht. Wer den Code liest, weiß ohne Suche, welche Rolle der Typ spielt.
  • Refactoring-Sicherheit. Wenn jemand das Interface ändert (zum Beispiel von Handle(Request) zu Handle(context.Context, Request)), bricht jeder Implementierer mit Compile-Check sofort. Ohne Check bricht nur der Aufrufer, der zufällig schon den neuen Signatur-Pfad nutzt — die alten Implementierungen rotten leise vor sich hin.

Der Code-Footprint ist minimal — eine Zeile pro Interface-Implementation, kein Laufzeit-Overhead, kein zusätzlicher Build-Schritt.

Embedding — wie Method Sets nach oben propagieren

Wenn ein Struct einen anderen Typ einbettet, erbt er dessen Method Set. Das ist nicht Vererbung im OO-Sinn — es ist Method Set Promotion. Die Spec sagt: Eingebettete Methoden werden Teil des Method Sets des äußeren Typs, als wären sie dort deklariert.

Go embedding-propagation.go
package main

import (
    "bytes"
    "fmt"
    "io"
)

// LogWriter bettet *bytes.Buffer ein und erbt damit Write.
type LogWriter struct {
    *bytes.Buffer
    prefix string
}

// Compile-Check: LogWriter ist io.Writer dank Embedding.
var _ io.Writer = (*LogWriter)(nil)

func main() {
    lw := &LogWriter{
        Buffer: &bytes.Buffer{},
        prefix: "[INFO] ",
    }
    // Write kommt aus *bytes.Buffer — ohne eigene Methode in LogWriter.
    fmt.Fprintln(lw, "Hallo Welt")
    fmt.Print(lw.String())
}
Output
Hallo Welt

Drei Punkte zur Embedding-Promotion:

  • Pointer-Embedding propagiert Pointer-Receiver-Methoden. LogWriter bettet *bytes.Buffer ein (nicht bytes.Buffer), darum stehen ihm alle Methoden von *bytes.Buffer zur Verfügung — inklusive Write, das Pointer-Receiver hat.
  • Promoted Methods landen im Method Set des äußeren Typs. Aus Sicht von Interface Satisfaction ist es, als hätte LogWriter Write selbst deklariert.
  • Überschreiben durch Re-Deklaration. Wenn du func (lw *LogWriter) Write(p []byte) (int, error) { ... } selbst schreibst, gewinnt die eigene Methode — die geerbte wird verschattet. Genau dieses Muster nutzen wir gleich im zweiten Praxis-Beispiel.

Interface erfüllt Interface — die Subset-Relation

Der zweite Punkt der Spec-Regel: „T is an interface and the type set of T is a subset of the type set of I." Was bedeutet das praktisch? Ein Interface erfüllt ein anderes Interface, wenn es mindestens dessen Methoden enthält. Damit lässt sich io.ReadCloser überall einsetzen, wo io.Reader verlangt wird — denn jeder Typ, der ReadCloser erfüllt (Read + Close), erfüllt auch Reader (nur Read).

Go interface-erfuellt-interface.go
package main

import (
    "fmt"
    "io"
    "strings"
)

// f nimmt io.Reader entgegen — die schmalere Schnittstelle.
func firstByte(r io.Reader) byte {
    buf := make([]byte, 1)
    _, _ = r.Read(buf)
    return buf[0]
}

func main() {
    // strings.NewReader gibt *strings.Reader zurück — erfüllt io.Reader.
    r := strings.NewReader("Hallo")
    fmt.Printf("%c\n", firstByte(r))

    // io.NopCloser gibt io.ReadCloser zurück.
    // Wir können den ReadCloser an einen Reader-Parameter geben,
    // weil io.ReadCloser jede io.Reader-Methode hat.
    rc := io.NopCloser(strings.NewReader("Welt"))
    fmt.Printf("%c\n", firstByte(rc))
}
Output
H
W

Die umgekehrte Richtung funktioniert nicht: io.Reader erfüllt io.ReadCloser nicht, weil ihm Close fehlt. Daher die Accept Interfaces, Return Structs-Faustregel — Funktionen sollten möglichst schmale Interfaces als Parameter verlangen, damit Aufrufer mehr Spielraum haben.

Praxis 1 — HTTP-Handler mit Compile-Check

net/http definiert das Interface http.Handler mit einer Methode:

Go http-handler-interface.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Ein eigener Router-Typ, der dieses Interface erfüllt, ist Schulbeispiel für das Compile-Check-Idiom in echtem Code:

Go router.go
package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "strings"
)

// Router ist unser eigener http.Handler.
type Router struct {
    routes map[string]http.HandlerFunc
}

func NewRouter() *Router {
    return &Router{routes: map[string]http.HandlerFunc{}}
}

func (r *Router) Handle(path string, h http.HandlerFunc) {
    r.routes[path] = h
}

// ServeHTTP — Pointer-Receiver, weil wir routes lesen.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    if h, ok := r.routes[req.URL.Path]; ok {
        h(w, req)
        return
    }
    http.NotFound(w, req)
}

// Compile-Time-Check — bricht ab, falls ServeHTTP fehlt
// oder die Signatur abweicht.
var _ http.Handler = (*Router)(nil)

func main() {
    r := NewRouter()
    r.Handle("/hello", func(w http.ResponseWriter, _ *http.Request) {
        fmt.Fprintln(w, "Hallo Welt")
    })

    // Test mit httptest — kein echter Server nötig.
    req := httptest.NewRequest("GET", "/hello", nil)
    rec := httptest.NewRecorder()
    r.ServeHTTP(rec, req)

    fmt.Print(strings.TrimSpace(rec.Body.String()))
}
Output
Hallo Welt

Was passiert, wenn du die Signatur kaputt machst? Ändere ServeHTTP(w http.ResponseWriter, req *http.Request) zu ServeHTTP(req *http.Request, w http.ResponseWriter) — Reihenfolge vertauscht. Der Compiler antwortet sofort mit *„Router does not implement http.Handler (wrong type for method ServeHTTP)". Ohne die Check-Zeile wäre der Build trotzdem grün — der Fehler träte erst dort auf, wo jemand http.ListenAndServe(":8080", r) ruft.

Praxis 2 — io.Writer-Wrapper mit Embedding und Override

Ein häufiges Muster: ein Wrapper, der einen bestehenden io.Writer umschließt, ein paar Zeichen vor- oder nachstellt und das ursprüngliche Interface unverändert anbietet. Embedding macht das knapp — die Override-Methode verschattet die geerbte:

Go prefix-writer.go
package main

import (
    "fmt"
    "io"
    "os"
)

// PrefixWriter wraps einen io.Writer und stellt Prefix vor jeden Write.
type PrefixWriter struct {
    io.Writer        // Embedding — Method Set von io.Writer wird vererbt
    prefix []byte
}

// Eigene Write-Methode VERSCHATTET die geerbte.
// Pointer-Receiver, damit *PrefixWriter weiterhin io.Writer erfüllt
// (siehe Compile-Check unten).
func (p *PrefixWriter) Write(b []byte) (int, error) {
    // Erst Prefix, dann Payload — beide gehen an den eingebetteten Writer.
    if _, err := p.Writer.Write(p.prefix); err != nil {
        return 0, err
    }
    return p.Writer.Write(b)
}

// Compile-Check — Verschattung darf io.Writer nicht brechen.
var _ io.Writer = (*PrefixWriter)(nil)

func main() {
    pw := &PrefixWriter{
        Writer: os.Stdout,
        prefix: []byte("[LOG] "),
    }
    fmt.Fprintln(pw, "Server gestartet")
    fmt.Fprintln(pw, "Port 8080")
}
Output
[LOG] Server gestartet
[LOG] Port 8080

Drei Beobachtungen:

  • Embedding ohne Override. Ohne die eigene Write-Methode würde *PrefixWriter die Write direkt aus dem eingebetteten io.Writer erben — der Prefix bliebe wirkungslos.
  • Verschattung. Die eigene Write-Methode hat Vorrang vor der geerbten — Go nennt diese Auflösung Method Resolution, sie geht von außen nach innen.
  • Compile-Check fängt Signatur-Drift. Wenn jemand den Rückgabewert auf error allein verkürzt, fängt die Check-Zeile das sofort ab.

Tools — gopls und implizite Interface-Implementierung

Weil Interfaces in Go implizit erfüllt werden, weiß der Editor bei einer Methodendefinition nicht automatisch, dass du gerade ein Standard-Interface implementierst. gopls, der offizielle Language Server, schließt diese Lücke mit zwei nützlichen Features:

  • Code Lens „implements". Über jeder Methode, die ein bekanntes Interface erfüllt, zeigt gopls den Hinweis, welches Interface das ist. Das ist die einzige praktische Möglichkeit, beim Lesen fremden Codes schnell zu sehen: „Aha, dieser Typ ist also ein http.Handler."
  • Go to Implementation / Go to Interface. Per Editor-Shortcut springst du von einer Interface-Methode zu allen Implementierungen und umgekehrt. Das ist das einzige Werkzeug, mit dem große Code-Bases mit dutzenden Implementierungen pro Interface beherrschbar bleiben.

Der Compile-Time-Check ist und bleibt aber das Werkzeug der ersten Wahl — er kostet nichts und liefert die präziseste Fehlermeldung von allen.

Häufige Stolperfallen

Value-Receiver-Mismatch — der Klassiker.

Du deklarierst func (b *Buffer) Write(...) und schreibst var w io.Writer = Buffer{} — Compile-Fehler „method has pointer receiver". Das Method Set von Buffer enthält Write nicht. Lösung: &Buffer{} zuweisen, oder den Receiver auf Value umstellen (wenn die Methode nicht mutiert).

Map-Elemente und Funktions-Rückgaben sind nicht adressierbar.

m["key"].Inc() mit Pointer-Receiver-Methode bricht ab — m["key"] ist nicht adressierbar, also kann Go kein implizites & einfügen. Workaround: in eine Variable umkopieren, oder gleich *T als Map-Wert verwenden (map[string]*Counter).

Compile-Check vergessen — Bug erst beim ersten Aufrufer.

Ohne var _ io.Writer = (*MyType)(nil) merkst du erst beim Aufrufer (http.ListenAndServe, io.Copy, …), dass dein Typ das Interface nicht erfüllt. Fehlermeldung steht dann bei fremdem Code, nicht bei deiner Definition. Eine Zeile pro Implementation, kein Laufzeit-Cost — gibt keinen Grund, es wegzulassen.

Embedding eines Werts statt eines Pointers verliert Pointer-Methoden.

type Foo struct { bytes.Buffer } bettet bytes.Buffer als Wert ein. Das Method Set von Foo enthält dann nur die Value-Receiver-Methoden von BufferWrite (Pointer-Receiver) fehlt. *Foo hat sie, weil *T-Method-Set jede T-Methode inklusive der promoted Pointer-Methoden enthält. Sicherer: *bytes.Buffer einbetten.

Stillschweigendes Verschatten beim Embedding.

Ein eingebetteter Typ und der äußere Typ haben beide eine Methode Close()? Die äußere gewinnt, ohne Warnung. Das ist gewollt, aber gefährlich beim Refactoring — wenn du der äußeren Methode später einen anderen Namen gibst, erbst du plötzlich die innere zurück, und das Verhalten kippt. Defensive Maßnahme: Compile-Check + Tests pro Interface-Methode.

Falsche Signatur bricht das Interface, ohne Methode zu entfernen.

Write(p []byte) error statt Write(p []byte) (int, error) — der Compiler sieht eine Methode Write, aber mit anderer Signatur. Du hast Write deklariert, dein Typ erfüllt io.Writer aber nicht. Ohne Compile-Check fällt das erst beim Aufruf auf. Mit Compile-Check sofort.

`var _ I = T{}` vs. `var _ I = (*T)(nil)` ist nicht austauschbar.

Wer einen Typ mit Value-Receiver-Methoden testet, nimmt T{}. Wer Pointer-Receiver hat, nimmt (*T)(nil). Wer sich vertut, kriegt entweder eine wertlose Aussage (Value-Check passt zwar, aber im Code wird *T zugewiesen) oder einen unnötigen Compile-Fehler. Beim Schreiben des Checks kurz mitdenken: welcher Typ wird im echten Code ans Interface zugewiesen?

Interface mit Pointer-Receivern verhindert Map-Keys.

Ein Interface ist als Map-Key zulässig, wenn der dahinterliegende konkrete Typ vergleichbar ist. Pointer sind vergleichbar (über Adresse), Werte ohne Slice/Map-Felder ebenfalls. Wer aber den Adress-Vergleich nicht will — zwei Instanzen mit gleichem Inhalt sind nicht „gleich" — sollte das Interface gar nicht erst als Map-Key nutzen, sondern den konkreten Typ.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Interfaces

Zur Übersicht