In Java, C# oder PHP musst du beim Definieren einer Klasse explizit ankündigen, welche Interfaces sie erfüllt: class FileWriter implements Writer, Closeable. Der Compiler prüft dann, ob die Methoden passen. In Go fehlt dieses Keyword vollständig — und das ist kein Versehen, sondern ein bewusst gewähltes Sprach-Design. Ein Typ implementiert ein Interface in Go implizit: sobald sein Methoden-Set alle Methoden des Interfaces enthält, erfüllt er es, ohne dass an einer einzigen Stelle deklariert werden müsste „dieser Typ gehört zu jenem Interface". Diese strukturelle Typisierung wirkt anfangs ungewohnt, ist aber der Schlüssel zu Gos berühmt-flachen Abhängigkeitsgraphen und dem Idiom „accept interfaces, return structs". Dieser Artikel zeigt, was implizit konkret bedeutet, welche Konsequenzen es für API-Design, Tooling und Performance hat — und wie du dir trotzdem einen statischen Erfüllungs-Beweis ins Programm schreibst.
Was heißt „implizit"?
In den meisten objektorientierten Sprachen ist die Beziehung zwischen Typ und Interface nominal: Sie existiert, weil der Programmierer sie benannt hat. Java verlangt class C implements I, C# class C : I, PHP class C implements I. Ohne dieses Schlüsselwort ist die Beziehung schlicht nicht da — und der Compiler weigert sich, einen C-Wert dort zu akzeptieren, wo ein I erwartet wird, selbst wenn C Methoden mit identischen Signaturen hätte.
Go geht den gegenteiligen Weg. Die Sprachspezifikation formuliert es im Abschnitt Interface types knapp:
A type
Timplements an interface if its method set is a superset of the interface. Variables of interface type can store values of any type with a method set that is any superset of the interface.
Heißt: Die Erfüllung wird strukturell geprüft. Wer die richtigen Methoden hat, erfüllt das Interface — egal, ob der Autor das wusste, wollte oder im selben Modul lebt. Das implements-Keyword existiert in Go nicht, und die Frage „welche Interfaces erfüllt mein Typ?" hat keine im Quellcode hinterlegte Antwort, nur eine berechnete.
// Java (explizit):
// class FileLogger implements Logger { ... }
//
// Go (implizit):
// type FileLogger struct { ... }
// func (f *FileLogger) Log(msg string) { ... }
//
// Ist *FileLogger ein Logger? Go-Antwort: ja — sofern Logger ein
// Interface mit genau einer Methode Log(string) ist. Weder
// FileLogger noch Logger müssen voneinander wissen.Der erste Reflex aus anderen Sprachen ist Misstrauen: „Wie kann der Compiler das prüfen, wenn ich es nicht hinschreibe?" Die Antwort ist: Er prüft es zu dem Zeitpunkt, an dem die Erfüllung gebraucht wird — bei einer Zuweisung, einem Funktions-Aufruf, einer Type-Assertion. Genau in diesem Moment vergleicht der Compiler die Method-Sets und entscheidet. Wenn die Methode fehlt, gibt es einen Compile-Fehler. Wenn alles passt, ist die Erfüllung lautlos da.
Strukturelle Typisierung — der formale Begriff
Was Go praktiziert, hat in der Typtheorie einen Namen: strukturelle Subtyp-Beziehung. Zwei Typen sind kompatibel, wenn ihre Struktur (bei Interfaces: das Methoden-Set) kompatibel ist — nicht, weil sie denselben Namen oder dieselbe Ableitungskette tragen. Der Gegensatz heißt nominale Subtyp-Beziehung: Java, C#, C++.
Die Spec über Method-Sets:
The method set of a type determines the interfaces that the type implements and the methods that can be called using a receiver of that type.
Konkret bedeutet das: Ein Interface ist nichts anderes als eine Menge von Methoden-Signaturen. Ein konkreter Typ erfüllt das Interface, wenn seine eigene Methoden-Menge diese Menge enthält (Obermenge). Mehr nicht. Es gibt keinen Vererbungsbaum, keine Registrierung, keine „interface table". Nur die Methoden.
package main
import "fmt"
// Interface — definiert eine Methoden-Menge.
type Greeter interface {
Greet() string
}
// Typ 1 — kennt Greeter nicht, hat aber die passende Methode.
type German struct{ Name string }
func (g German) Greet() string {
return "Hallo, " + g.Name + "!"
}
// Typ 2 — ebenfalls ohne Wissen über Greeter.
type French struct{ Name string }
func (f French) Greet() string {
return "Bonjour, " + f.Name + " !"
}
// Konsument — nimmt das Interface entgegen, kennt die konkreten Typen nicht.
func sayHello(g Greeter) {
fmt.Println(g.Greet())
}
func main() {
sayHello(German{Name: "Anna"})
sayHello(French{Name: "Pierre"})
}Hallo, Anna!
Bonjour, Pierre !Weder German noch French deklarieren irgendwo, dass sie Greeter erfüllen. Sie haben einfach eine Greet() string-Methode — das reicht. Genauso wenig deklariert sayHello, welche konkreten Typen es akzeptiert. Es verlangt das Interface, und alles, was die Methoden-Menge passend hat, kommt durch.
Diese Art der Kompatibilität wird oft mit dem Schlagwort Duck Typing beschrieben („wenn es wie eine Ente quakt..."), allerdings mit einem wichtigen Unterschied: Pythons Duck Typing ist dynamisch — der Fehler kommt zur Laufzeit, wenn die Methode fehlt. Gos Duck Typing ist statisch — der Compiler prüft die Methoden-Menge zur Compile-Zeit, du siehst den Fehler vor dem Start. Manche Autoren nennen das Structural Typing oder Compile-Time Duck Typing, um den Unterschied zu betonen.
Das Standard-Beispiel — fmt.Stringer
Das vielleicht häufigste Interface in der Go-Welt ist fmt.Stringer. Es besteht aus genau einer Methode:
// Aus dem Paket fmt der Standard-Bibliothek:
type Stringer interface {
String() string
}Jeder Typ, der eine String() string-Methode anbietet, erfüllt fmt.Stringer — automatisch. Und fmt.Println, fmt.Printf("%v"), fmt.Sprint rufen diese Methode auf, sobald sie auf einen Wert treffen, der das Interface erfüllt:
package main
import "fmt"
type Currency struct {
Code string
Symbol string
}
// Eine Methode — fertig. Currency erfüllt fmt.Stringer.
func (c Currency) String() string {
return c.Symbol + " (" + c.Code + ")"
}
type Temperature float64
func (t Temperature) String() string {
return fmt.Sprintf("%.1f °C", float64(t))
}
func main() {
eur := Currency{Code: "EUR", Symbol: "€"}
t := Temperature(21.7)
// fmt erkennt das Stringer-Interface und ruft String() auf.
fmt.Println(eur) // € (EUR)
fmt.Println(t) // 21.7 °C
fmt.Printf("Heute: %v\n", t) // Heute: 21.7 °C
}€ (EUR)
21.7 °C
Heute: 21.7 °CBeachte: Currency importiert das fmt-Paket nicht. Temperature ebenso wenig. Die Typen haben null Kenntnis vom Stringer-Interface. Trotzdem funktioniert die Integration — weil fmt.Println intern eine Type-Assertion if s, ok := arg.(Stringer); ok { ... } macht und die Methode aufruft, sobald sie da ist.
Das ist die volle Kraft der impliziten Implementierung in einem Satz: Du erweiterst das Verhalten von Stdlib-Funktionen, ohne ihren Quellcode anzufassen, ohne dich registrieren zu müssen, ohne dass dein Paket das Stringer-Interface überhaupt nennen muss.
„Discovery" — Effective Go's eigentlich entscheidender Satz
Effective Go formuliert die wichtigste Designkonsequenz im Abschnitt Interfaces and other types in einem einzigen Satz, der sich lohnt zu zitieren:
One important category of type conversions are interface conversions and interface satisfaction. The implicit notion of interface satisfaction in Go means that interfaces are discovered, not declared.
„Interfaces are discovered, not declared." Das ist nicht nur eine Beobachtung über Syntax, sondern ein Designprinzip: Du schreibst zuerst die konkreten Typen mit ihren Methoden, und erst später — wenn ein Konsument die Funktionalität braucht — entsteht das Interface. Das Interface fasst zusammen, was schon da war; es zwingt dem konkreten Typ nichts auf.
In Java läuft die Reihenfolge oft umgekehrt: Erst das Interface, dann die Klasse, die es implementiert. Das führt zu langen Vererbungs-Hierarchien und „interface-first"-Bibliotheken, in denen jede konkrete Klasse vorab in einen Vertrag gepresst wird. Go-Code sieht typischerweise so aus:
// Schritt 1 — Du schreibst einen konkreten Typ.
type DiskCache struct{ /* ... */ }
func (d *DiskCache) Get(key string) ([]byte, error) { /* ... */ return nil, nil }
func (d *DiskCache) Set(key string, value []byte) error { /* ... */ return nil }
// Schritt 2 — irgendwo später, vielleicht in einem anderen Paket,
// entdeckt jemand: „ich brauche nur Get und Set". Er definiert das
// Interface dort, wo er es benötigt:
type KeyValueStore interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
}
func warmCache(kv KeyValueStore, keys []string) { /* ... */ }DiskCache wusste beim Schreiben nichts von KeyValueStore. Trotzdem erfüllt es das Interface — und kann ohne Anpassung an warmCache übergeben werden. Wenn morgen ein zweites Backend (RedisCache, MemoryCache) entsteht, erfüllt es das Interface ebenfalls automatisch, sobald es die zwei Methoden anbietet.
Interfaces beim Konsumenten definieren
Aus „discovered, not declared" folgt die wohl bekannteste Go-Konvention: Interfaces gehören zum Konsumenten, nicht zum Produzenten. Wer einen Typ schreibt, der I/O macht, definiert nicht ein eigenes MyWriter-Interface daneben. Wer eine Funktion schreibt, die nur lesen will, definiert das Reader-Interface dort, wo die Funktion lebt.
Konkret heißt das:
- Schlecht. Das Paket
databaseexportiert einDBClient-Interface mit allen 47 Methoden, die seine konkrete Implementierung anbietet. Konsumenten importieren das Interface und mocken es — und müssen alle 47 Methoden im Mock stubben, obwohl sie nur drei brauchen. - Gut. Das Paket
databaseexportiert nur den konkreten Typ. Der Konsument im Paketreportdefiniert sich ein winzigesuserLookup-Interface mit den zwei Methoden, die er nutzt. Sein Mock im Test hat zwei Methoden, nicht 47.
// Paket database (Produzent) — exportiert konkreten Typ:
package database
type Client struct{ /* viele Felder */ }
func (c *Client) GetUser(id int64) (User, error) { /* ... */ }
func (c *Client) CreateUser(u User) error { /* ... */ }
func (c *Client) DeleteUser(id int64) error { /* ... */ }
// ... 44 weitere Methoden// Paket report (Konsument) — definiert sein eigenes, schmales Interface:
package report
// userSource — exakt was dieser Code braucht. Nicht mehr.
type userSource interface {
GetUser(id int64) (User, error)
}
func WeeklyReport(src userSource, ids []int64) Report {
// arbeitet nur mit src.GetUser
}*database.Client erfüllt userSource automatisch — keine Anpassung am database-Paket nötig. Im Test kann report ein winziges Mock mit einer Methode bauen. Das ist die direkte Konsequenz aus impliziter Implementierung: Interfaces dürfen klein sein, weil sie nicht im Voraus alle Bedürfnisse antizipieren müssen.
Dieses Idiom wird oft als „accept interfaces, return structs" zusammengefasst — Funktionen nehmen Interfaces (klein, beim Konsumenten definiert), geben aber konkrete Typen zurück. Der eigene Artikel dazu vertieft das.
Vorteile und Nachteile
Implizite Implementierung ist keine Gratis-Verbesserung gegenüber dem implements-Modell. Sie bringt klare Vorteile und ebenso klare Kosten — beides gehört auf den Tisch.
| Aspekt | Implizit (Go) | Explizit (Java/C#) |
|---|---|---|
| Decoupling | Sehr stark — Typ und Interface kennen sich nicht | Schwach — Typ nennt Interface namentlich |
| Retroactive Implementation | Möglich — fremde Typen können neue Interfaces erfüllen | Unmöglich ohne Adapter-Klasse |
| Interface-Granularität | Sehr klein („Rule of one method") | Tendenziell breit |
| Discoverability | Schwach — keine Liste „welche Interfaces erfüllt T?" | Stark — IDE zeigt sofort an |
| Tooling-Aufwand | Statische Analyse muss Method-Sets ableiten | Direkt aus Source ablesbar |
| Risiko unbeabsichtigter Erfüllung | Real — gleiche Signatur reicht | Praktisch nicht vorhanden |
Vorteile. Der Hauptgewinn ist Entkopplung: Pakete bleiben unabhängig, weil sie keine Interface-Importe brauchen, um zusammenzuspielen. Die zweite Stärke ist die retroaktive Möglichkeit — du kannst ein Interface definieren, das von Stdlib- oder Third-Party-Typen erfüllt wird, ohne deren Code zu ändern. Die dritte ist Granularität: Weil Interfaces beim Konsumenten entstehen, bleiben sie klein und auf einen Zweck zugeschnitten („the bigger the interface, the weaker the abstraction" — Rob Pike).
Nachteile. Die Kehrseite ist die Discoverability: In Java sagt dir die IDE in einer Sekunde, welche Interfaces eine Klasse implementiert. In Go ist die Antwort nicht im Quellcode hinterlegt — Tools wie gopls, guru oder die Implementations-Funktion in IDEs müssen sie aus Method-Sets ableiten. Das funktioniert, ist aber langsamer und manchmal unvollständig.
Der zweite Nachteil ist ein subtiles Risiko: unbeabsichtigte Erfüllung. Ein Typ, der nebenher eine Methode mit Signatur String() string bekommt, fängt plötzlich an, fmt.Stringer zu erfüllen — und das Output-Verhalten von fmt.Println ändert sich, eventuell ungewollt. In der Praxis ist das selten, weil Method-Signaturen zwei Komponenten haben (Name und Typen), die zufällig schwer zu kollidieren sind. Aber als Möglichkeit existiert es.
Compile-Time-Check — der explizite Erfüllungs-Beweis
Wenn dir die fehlende Sichtbarkeit Sorgen macht — wenn du im Code dokumentieren willst, dass ein bestimmter Typ ein bestimmtes Interface erfüllen muss — bietet Go ein elegantes Idiom: die Compile-Time-Assertion. Das ist eine globale Variable, die nur zur Compile-Zeit existiert und einen Pointer-Wert dem Interface zuweist:
package main
import "fmt"
type FileLogger struct {
path string
}
func (f *FileLogger) String() string {
return "FileLogger(" + f.path + ")"
}
// Compile-Time-Assertion: *FileLogger MUSS fmt.Stringer erfüllen.
// Wenn die String()-Methode entfernt oder ihre Signatur geändert
// wird, schlägt der Build hier fehl — laut und sofort.
var _ fmt.Stringer = (*FileLogger)(nil)
func main() {
f := &FileLogger{path: "/var/log/app.log"}
fmt.Println(f)
}Die Zeile zerlegt:
var _— eine Variable mit Namen_(Blank Identifier). Sie wird nicht im Programm verwendet, der Compiler erlaubt sie aber trotzdem.fmt.Stringer =— der Typ der Variablen ist das Interface, das geprüft werden soll.(*FileLogger)(nil)— ein typed nil-Pointer vom Typ*FileLogger. Er hat keinen Speicher-Footprint, aber den Typ, den der Compiler für die Method-Set-Prüfung braucht.
Effekt: Wenn *FileLogger das Interface nicht (mehr) erfüllt, schlägt der Build mit einer klaren Fehlermeldung fehl: cannot use (*FileLogger)(nil) ... as fmt.Stringer in assignment: missing method String. Du hast die Erfüllung dokumentiert, ohne einen Runtime-Overhead zu erzeugen und ohne die Sprach-Idiomatik zu brechen.
Die Stdlib nutzt dieses Pattern an vielen Stellen. Beispielsweise findest du im archive/zip-Paket Zeilen wie var _ io.ReaderAt = (*Reader)(nil). Das ist sowohl für menschliche Leser ein Hinweis als auch für den Compiler eine Garantie.
Implementations-Detail — was passiert zur Laufzeit?
Implizite Implementierung ist ein Compile-Time-Konzept — der Vergleich der Method-Sets findet beim Übersetzen statt. Trotzdem hat das Interface-Konstrukt zur Laufzeit eine konkrete Darstellung, die für Performance-Überlegungen relevant ist.
Ein Interface-Wert in Go besteht intern aus zwei Wörtern (iface-Header):
- Ein Pointer auf eine itab (interface table) — eine vom Compiler/Runtime erzeugte Struktur, die für die Kombination (konkreter Typ, Interface) eine Sprung-Tabelle der Methoden enthält.
- Ein Pointer auf die konkreten Daten — den Wert hinter dem Interface.
// Konzeptuell — so sieht die Runtime-Repräsentation aus:
type iface struct {
tab *itab // Pointer auf interface table
data unsafe.Pointer // Pointer auf den konkreten Wert
}
type itab struct {
inter *interfacetype // Beschreibung des Interface-Typs
_type *_type // Beschreibung des konkreten Typs
hash uint32
fun [N]uintptr // Methoden-Pointer
}Konsequenzen für die Praxis:
- Method-Call über Interface ist eine indirekte Adressierung. Statt eines direkten Funktions-Aufrufs läuft jeder Methodenaufruf auf einem Interface-Wert über die
itab— Lookup, Sprung, Aufruf. Bei hot loops mit Millionen Aufrufen pro Sekunde kann das messbar sein gegenüber einem direkten Aufruf auf dem konkreten Typ. - Inlining ist erschwert. Der Compiler kann Funktions-Inlining bei Interface-Calls oft nicht durchführen, weil zur Compile-Zeit nicht feststeht, welche konkrete Methode dahinterliegt. Bei direkten Calls auf konkrete Typen geht Inlining problemlos.
- Interface-Wert ist immer 16 Byte.
unsafe.Sizeof(io.Reader(nil))liefert 16 (auf 64-Bit). Das ist der Platz für die zwei Pointer, unabhängig davon, ob der Interface-Wert ein 4-Byte-intoder einen 4 KB großen Struct kapselt.
In den allermeisten Anwendungsfällen ist der Overhead unbedeutend — Interfaces sind in der Stdlib allgegenwärtig, und Go-Programme sind dadurch nicht langsam. Aber wer wirklich enge Schleifen schreibt, sollte wissen, dass „auf konkreten Typ statt auf Interface arbeiten" eine reale Optimierungs-Hebel ist.
Ein Typ, mehrere Interfaces
Weil die Erfüllung strukturell und nicht namentlich gebunden ist, kann ein einziger Typ beliebig viele Interfaces gleichzeitig erfüllen — er muss dafür nichts deklarieren. Das ist die Grundlage, auf der *os.File als universeller I/O-Baustein funktioniert:
package main
import (
"fmt"
"io"
"os"
)
// Wir wollen demonstrieren, welche Interfaces *os.File erfüllt.
// Jede dieser Zuweisungen ist ein Compile-Time-Check.
var (
_ io.Reader = (*os.File)(nil)
_ io.Writer = (*os.File)(nil)
_ io.Closer = (*os.File)(nil)
_ io.Seeker = (*os.File)(nil)
_ io.ReaderAt = (*os.File)(nil)
_ io.WriterAt = (*os.File)(nil)
_ io.ReadCloser = (*os.File)(nil)
_ io.ReadWriter = (*os.File)(nil)
)
func main() {
f, _ := os.Open("/etc/hosts")
defer f.Close()
fmt.Println("erfüllt mehrere Interfaces:", f.Name())
}In einem realen Programm bedeutet das: *os.File erfüllt acht Interfaces gleichzeitig, ohne dass im File-Code irgendwo implements steht. Jede Funktion, die ein passendes Interface erwartet, akzeptiert ein File-Handle — ohne dass der Stdlib-Autor diese Funktionen vorhersehen musste.
Praxis 1 — io.Reader/io.Writer als Lingua Franca der Stdlib
Der vielleicht wichtigste Effekt impliziter Interfaces zeigt sich an io.Reader und io.Writer. Beide sind winzige Interfaces — eine Methode jeweils:
// Aus der Stdlib (Paket io):
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}Genau diese Kleinheit ist die Stärke. Was alles io.Reader erfüllt — ohne dass diese Typen vom io-Paket wussten, als sie geschrieben wurden: *os.File, *bytes.Buffer, *bytes.Reader, *strings.Reader, *bufio.Reader, *gzip.Reader, *tar.Reader, *http.Body, *net.TCPConn, *tls.Conn, *crypto/rand.Reader. Dazu Drittanbieter-Bibliotheken: S3-Object-Streams, MinIO-Reader, GCS-Reader, ZIP-Datei-Reader, kombinatorische io.MultiReader-Konstruktionen.
Jeder davon kommt durch jede Funktion durch, die ein io.Reader entgegennimmt — io.Copy, io.ReadAll, bufio.NewScanner, json.NewDecoder, xml.NewDecoder, csv.NewReader, gzip.NewReader, crypto/sha256.New().Write(...) (via Writer-Interface).
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
// Eine einzige Funktion — funktioniert mit allem, was io.Reader erfüllt.
func countBytes(r io.Reader) (int, error) {
data, err := io.ReadAll(r)
return len(data), err
}
func main() {
// Aus einem String:
n1, _ := countBytes(strings.NewReader("Hallo Welt"))
fmt.Println("String-Reader:", n1)
// Aus einem Byte-Buffer:
buf := bytes.NewBufferString("abc def ghi")
n2, _ := countBytes(buf)
fmt.Println("Bytes-Buffer: ", n2)
}String-Reader: 10
Bytes-Buffer: 11Die Funktion countBytes weiß nichts über Strings, Bytes oder Dateien. Sie nimmt einen io.Reader — und wer den erfüllt, kommt durch. Genau dieses Pattern macht Go-Code im I/O-Bereich extrem komponierbar: Du steckst Reader und Writer wie Pipe-Stücke aneinander, weil jede Stufe der Pipeline implizit das passende Interface erfüllt.
Praxis 2 — eigener slog.Handler
Seit Go 1.21 ist log/slog die idiomatische Strukturlog-Bibliothek. Im Kern steht das Interface slog.Handler, das du implizit erfüllen kannst — und damit dein eigenes Log-Format, dein eigenes Backend, deinen eigenen Test-Logger bauen. Das Interface hat vier Methoden:
// Aus log/slog (vereinfacht gezeigt):
type Handler interface {
Enabled(ctx context.Context, level Level) bool
Handle(ctx context.Context, r Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}Ein eigener Handler, der jede Log-Zeile als eine Zeile menschen-lesbares Format auf einen Writer schreibt — ohne dass unser Code das slog-Paket außer für die Typen irgendwo „implementieren" deklariert:
package main
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"strings"
)
// PrettyHandler — eigener Handler. Erfüllt slog.Handler implizit.
type PrettyHandler struct {
w io.Writer
level slog.Level
attrs []slog.Attr
}
func NewPrettyHandler(w io.Writer, level slog.Level) *PrettyHandler {
return &PrettyHandler{w: w, level: level}
}
func (h *PrettyHandler) Enabled(_ context.Context, lvl slog.Level) bool {
return lvl >= h.level
}
func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error {
var sb strings.Builder
fmt.Fprintf(&sb, "[%s] %-5s %s",
r.Time.Format("15:04:05"), r.Level.String(), r.Message)
for _, a := range h.attrs {
fmt.Fprintf(&sb, " %s=%v", a.Key, a.Value)
}
r.Attrs(func(a slog.Attr) bool {
fmt.Fprintf(&sb, " %s=%v", a.Key, a.Value)
return true
})
sb.WriteByte('\n')
_, err := io.WriteString(h.w, sb.String())
return err
}
func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
cp := *h
cp.attrs = append(append([]slog.Attr{}, h.attrs...), attrs...)
return &cp
}
func (h *PrettyHandler) WithGroup(name string) slog.Handler {
return h // vereinfacht — echte Implementierung würde Prefix bauen
}
// Compile-Time-Beweis, dass wir das Interface wirklich erfüllen.
var _ slog.Handler = (*PrettyHandler)(nil)
func main() {
logger := slog.New(NewPrettyHandler(os.Stdout, slog.LevelInfo))
logger.Info("server gestartet", "port", 8080, "env", "prod")
logger.Warn("langsame Antwort", "ms", 421, "path", "/api/v1/items")
}[12:00:00] INFO server gestartet port=8080 env=prod
[12:00:00] WARN langsame Antwort ms=421 path=/api/v1/itemsDie Schlüsselzeile ist var _ slog.Handler = (*PrettyHandler)(nil). Sie macht die Erfüllung sichtbar und prüfbar — ohne dass PrettyHandler jemals das Wort implements benutzt. Wenn morgen slog.Handler eine Methode hinzubekommt, schlägt der Build hier auf, und du weißt, wo du nacharbeiten musst.
Genau dieses Pattern — eigene Implementierung, var _ Interface = (*T)(nil), fertig — ist in Go-Codebases extrem verbreitet: HTTP-Middleware, Test-Mocks, Plugin-Adapter, Cache-Backends, Transport-Layer. Wer das Interface des Stdlib-Konsumenten erfüllt, hat sofort den Anschluss an dessen Ökosystem.
Besonderheiten
Implizite Erfüllung ist ein Compile-Time-Konzept, kein Runtime-Trick.
Der Compiler prüft beim Zuweisen, Übergeben oder Returnen, ob das Method-Set des konkreten Typs eine Obermenge des Interface-Method-Sets ist. Schlägt die Prüfung fehl, gibt es einen Build-Fehler — keine Runtime-Überraschung. Das unterscheidet Gos Ansatz vom dynamischen Duck Typing in Python/Ruby, wo der gleiche Fehler erst beim Aufruf der fehlenden Methode auffällt.
var _ Interface = (*T)(nil) ist das Standard-Idiom für Erfüllungs-Beweise.
Wer dokumentieren will, dass ein Typ ein Interface erfüllen muss, schreibt diese Zeile auf Paket-Ebene. Sie hat keinen Runtime-Overhead (typed nil ohne Allokation), bleibt aber Compile-Time-prüfbar. Stdlib und große Frameworks (gRPC, Kubernetes, etcd) nutzen das durchgängig — übernimm es.
Pointer- und Value-Receiver beeinflussen, wer das Interface erfüllt.
Eine func (t *T) M()-Methode landet im Method-Set von *T, aber nicht im Method-Set von T. Wer var i Interface = myValue schreibt, wo myValue vom Typ T ist, bekommt einen Compile-Fehler, wenn Interface eine Pointer-Receiver-Methode verlangt. Lösung: var i Interface = &myValue. Klassischer Erst-Bug bei Interfaces.
„Interfaces are discovered, not declared" ist das Designprinzip dahinter.
Effective Go's Schlüsselsatz erklärt die Reihenfolge: Erst konkrete Typen, dann das Interface beim Konsumenten — nicht umgekehrt. Wer Java-Gewohnheiten mitbringt und versucht, Interface-first zu designen, baut breite Interfaces ohne Nutzer und vermisst das Decoupling-Potenzial. Klein anfangen, beim Konsumenten leben lassen.
Kleine Interfaces („the bigger the interface, the weaker the abstraction").
Rob Pikes Faustregel. io.Reader mit einer Methode ist mächtiger als ein hypothetisches Storage-Interface mit 47 Methoden — weil der Reader von tausend Typen erfüllt wird, das große Storage von zweien. Die Stdlib zeigt das durch: Reader, Writer, Closer, Stringer, Error — alle haben eine Methode. Komposition aus kleinen Interfaces ist der idiomatische Weg.
Interface-Werte sind 16 Byte — zwei Pointer.
Ein Wert vom Interface-Typ besteht zur Laufzeit aus (itab, data). Das macht jedes Interface-Argument 16 Byte schwer, unabhängig von der Größe des dahinterliegenden konkreten Werts. Ein Methodenaufruf darauf ist eine indirekte Adressierung — in 99 % der Fälle vernachlässigbar, in hot loops messbar. Wer auf Mikrosekunden achtet, arbeitet im inneren Kern direkt auf dem konkreten Typ.
Discoverability ist Gos schwache Seite — Tooling gleicht aus.
Anders als Java zeigt der Quellcode nicht an, welche Interfaces ein Typ erfüllt. gopls (LSP-Server) bietet aber „Find Implementations" und „Go to Interface" — VS Code, GoLand und Neovim unterstützen es. Bei großen Codebases ist das Tool unverzichtbar; im Code selbst hilft die var _ Interface = ...-Zeile als Hinweis für menschliche Leser.
Implizite Erfüllung erlaubt retroaktive Erweiterung externer Typen.
Du kannst ein eigenes Interface definieren, das von Typen erfüllt wird, die du nicht besitzt — *sql.DB, *http.Request, beliebige Stdlib-Strukturen. In Java geht das nur über Adapter-Klassen, die jede einzelne Methode weiterleiten. Das ist einer der praktisch wirkungsvollsten Vorteile struktureller Typisierung, wird in Tutorials aber selten betont.
Weiterführende Ressourcen
Externe Quellen
- Interface types — Go Language Specification
- Method sets — Go Language Specification
- Effective Go: Interfaces
- Effective Go: Interfaces and other types
- Go Proverbs (Rob Pike) — „The bigger the interface, the weaker the abstraction"
- Go Wiki: CodeReviewComments — Interfaces
log/slog— Package Documentation
Verwandte Artikel
- Interfaces — Übersicht und Method-Sets
- Das leere Interface —
interface{}undany - Type Assertion und Type Switch
- Interface-Satisfaction im Detail
- „Accept interfaces, return structs" — das Konsumenten-Idiom
- nil-Interface-Fallen — typed nil vs. untyped nil
- Methoden auf Structs — Receiver-Grundlagen
- Value- vs. Pointer-Receiver — Auswirkung auf Method-Sets