Interfaces beschreiben in Go einen Satz von Methoden, den ein Typ bereitstellen kann. Sie binden Code an Verhalten statt an konkrete Strukturen, sodass Implementierungen durch jeden Typ erfüllt werden können, der die geforderten Methodensignaturen bietet. Die Definition erfolgt schlank über Schlüsselwort interface, die Zuordnung eines Typs zu einem Interface geschieht implizit durch passende Methoden.
Einführung
Interfaces sind ein ziemlich elegantes Konzept in Go. Sie bilden das Fundament für Polymorphismus, lose Kopplung und testbaren Code. Im Gegensatz zu einigen anderen Sprachen funktionieren Interfaces in Go implizit - es gibt keine explizite Deklaration.
Zentrale Idee
Ein Interface definiert ein Verhalten (eine Menge von Methoden), nicht eine Implementierung. Jeder Typ, der alle Methoden eines Interfaces implementiert, erfüllt dieses Interface automatisch, ohne dass der Typ wissen muss, dass das Interface überhaupt existiert.
Oft wird diese Herangehensweise als “Duck Typing” bezeichnet.
“If it walks like a duck and quacks like a duck, it’s a duck.” In Go:
“Wenn ein Typ die Methoden eines Interfaces hat, dann erfüllt er dieses Interface.” Wofür könnten Interfaces gut sein?
- Abstraktion: Code gegen Interfaces schreiben, nicht gegen konkrete Typen
- Flexibilität: Verschiedene Implementierungen austauschbar machen
- Testbarkeit: Mocks und Stubs einfach erstellen
- Lose Kopplung: Abhängigkeiten reduzieren
- Komposition: Kleine Interfaces kombinieren (Interface-Komposition)
Was ist ein Interface?
Ein Interface ist ein Typ, der eine Menge von Methoden-Signaturen definiert. Es beschreibt was ein Typ können muss, nicht wie er es tut.
Grundlegende Definition
type InterfaceName interface {
MethodName(parameter Typ) RückgabeTyp
// Weitere Methoden ...
}Hier ein sehr einfaches Beispiel.
type Speaker interface {
Speak() string
}Dieses Interface definiert ein Verhalten. Jeder Typ, der eine Methode Speak() hat, die einen string zurückgibt, erfüllt das Speaker Interface.
Werfen wir einen Blick auf ein vollständiges Beispiel und eine Möglichkeit zur Implementierung.
package main
import (
"fmt"
"errors"
)
type PaymentTerminal interface {
Pay(amount float64) error
}
type MoneyTerminal struct {
Id int
Name string
Balance float64
}
type CreditTerminal struct {
MoneyTerminal
}
func (ct *CreditTerminal) Pay(amount float64) error {
if ct.Balance < amount {
return errors.New("nicht ausreichendes Guthaben")
}
ct.Balance -= amount
fmt.Printf("Credit Terminal zahlt Scheine aus: %.2f\n", amount)
return nil
}
type CoinTerminal struct {
MoneyTerminal
}
func (ct *CoinTerminal) Pay(amount float64) error {
if ct.Balance < amount {
return errors.New("nicht ausreichendes Guthaben")
}
ct.Balance -= amount
fmt.Printf("Coin Terminal zahl MÜNZEN aus: %.2f\n", amount)
return nil
}
func main() {
creditTerminal := &CreditTerminal{
MoneyTerminal: MoneyTerminal{
Id: 0001,
Name: "CRDTRM",
Balance: 12400,
},
}
if err := creditTerminal.Pay(2330); err != nil {
fmt.Println("Auszahlung erfolgreich")
}
coinTerminal := &CoinTerminal{
MoneyTerminal: MoneyTerminal{
Id: 0002,
Name: "CNTRM",
Balance: 5430,
},
}
if err := coinTerminal.Pay(200); err != nil {
fmt.Println("Auszahlung erfolgreich")
}
}
Credit Terminal zahlt Scheine aus: 2330.00
Coin Terminal zahl MÜNZEN aus: 200.00Ok, betrachten wir mal, was in diesem Beispiel alles passiert.
Wir definieren unser Interface PaymentTerminal. Dieses Interface sieht eine Methode Pay(amount float64) error vor. Also, eine Methode, die einen Parameter vom Typ float64 annehmen muss und einen Fehler (oder nil) zurückgibt.
Weiter im Verlauf definieren wir zwei Typen. CreditTerminal und CoinTerminal. Eigentlich definieren wir sogar drei Typen. Wir verwenden hier Struct Embedding, da die Struktur (Felder) bei beiden Typen gleich sind, können wir einen übergreifenden Typ erstellen.
Für jeden dieser Typen definieren wir eine Methode Pay(), welche eine identische Signatur mit der Methode im Interface hat. Dabei können die Funktionen völlig unterschiedliche Dinge tun. Der Funktionsinhalt kann sich also komplett unterscheiden. Solange sie (die Typ-gebundenen Funktionen) einen Parameter vom Typ float64 annehmen und einen Fehler zurückgeben, erfüllen sie das Interface.
Interface-Definition und Syntax
Methodensignaturen
Obwohl bereits weiter oben erwähnt, sprechen wir hier den Aufbau (im Kontext der Signatur) erneut an.
Ein Interface besteht aus Methodensignaturen (Name, Parameter, Rückgabewerte), aber ohne Implementierung.
Syntax
type InterfaceName interface {
Method1(param1 Type1, param2 Type2) ReturnType
Method2() (ReturnType1, ReturnType2)
Method3(param Type) error
}Beispiel
type Writer interface {
Write(data []byte) (int, error)
}Wichtige Regeln für Interfaces
- Methodennamen müssen exportiert sein (Großschreibung), wenn das Interface exportiert ist
- Parameter und Rückgabewerte müssen vollständig definiert sein
- Parameternamen sind optional (können weggelassen werden)
Beispiel: Mit und ohne Parameternamen
// Mit Parameternamen (üblich für Dokumentation)
type Reader interface {
Read(p []byte) (n int, err error)
}
// Ohne Parameternamen (kürzer, aber weniger dokumentativ)
type Reader interface {
Read([]byte) (int, error)
}Empty Interface - interface{} und any
Das Empty Interface (interface{}) ist ein Interface ohne Methoden. Da jeder Typ null oder mehr Methoden hat, erfüllt jeder Typ das Empty Interface.
Definition
interface{}Ab Go 1.18 gibt es den Alias any, der identisch zu interface{} ist.
type any = interface{}Schauen wir uns, wie man das Empty Interface verwenden kann.
package main
import "fmt"
func main() {
var i interface{}
i = 42
fmt.Println(i)
i = "Hallo"
fmt.Println(i)
i = true
fmt.Println(i)
i = []int{1, 2, 3}
fmt.Println(i)
}42
Hallo
true
[1 2 3]Im Grunde könnten wir auch folgendermaßen unsere i Variable definieren.
var i anyWann bietet es sich an, interface{}/any zu verwenden?
- Wenn der Typ zur Compile-Zeit unbekannt ist
- Bei generischen Datenstrukturen (nach Go 1.18 Generics bevorzugen)
- Bei Dekodierung (z.B. JSON in unbekannte Struktur)
- Bei Reflection
Schauen wir uns ein Beispiel an, wie man JSON in map[string]interface{} dekodieren kann.
package main
import (
"fmt"
"encoding/json"
)
func main() {
jsonData := `{
"name": "John",
"age": 30,
"active": true
}`
var result map[string]interface{}
json.Unmarshal([]byte(jsonData), &result)
fmt.Println(result["name"])
fmt.Println(result["age"])
fmt.Println(result["active"])
}John
30
trueImplizite Implementierung
Der größte Unterschied zu manchen anderen Sprachen: In Go gibt es keine explizite Deklaration, dass ein Typ ein Interface implementiert.
Wenn wir also eine oder alle Methoden einem Typ zuweisen (Receiver verwenden) und diese Methoden in der gleichen Konstellation bei einem Interface definiert sind, implementiert unser Typ automatisch dieses Interface.
type Vehicle interface {
Drive(miles int)
}
type Car struct{
Vendor string
}
func (c *Car) Drive(miles int) {
fmt.Println("Car drive:", miles)
}Interface-Werte - Interna
Ein Interface-Wert besteht intern aus zwei Komponenten:
- Dynamischer Typ: Der konkrete Typ des gespeicherten Werts
- Dynamischer Wert: Der tatsächliche Wert
Die interne Repräsentation - iface und eface
Go unterscheidet intern zwischen zwei Interface-Typen.
1. eface (Empty Interface): Für interface{}/any
type eface struct {
_type *_type // Pointer auf Typ-Informationen
data unsafe.Pointer // Pointer auf den tatsächlichen Wert
}2. iface (Interface mit Methoden): Für alle anderen Interfaces
type iface struct {
tab *itab // Pointer auf Interface-Tabelle (Typ + Methode)
data unsafe.Pointer // Pointer auf den tatsächlichen Wert
}Was ist die itab (Interface Table)?
Die itab ist das Herzstück der Interface-Implementierung in Go. Sie enthält Folgendes.
type itab struct {
inter *interfacetype // Informationen über das Interface selbst
_type *_type // Informationen über den konkreten Typ
hash uint32 // Kopie von _type.hash (für schnellen Type-Switch)
_ [4]byte // Padding
fun [1]uintptr // Variable-size array von Method-Pointern
}Visualisiert sieht das Konstrukt wie folgt aus.
var v Vehicle = Car{Vendor: "BMW"}
+-----------------------+
| v (Vehicle) |
+-----------------------+
| tab *itab ----+----> +-----------------------------+
| | | inter: *Vehicle-Type |
| | | _type: *Car-Type |
| | | hash: 0x12345678 |
| | | fun[0]: Drive-Method ------+--> func (c Car) Drive()
| | +-----------------------------+
+-----------------------+
| data unsafe.Pointer --+----> Car{Vendor: "BMW"}
+-----------------------+Warum diese Struktur?
- Typ-Information und Wert getrennt: Ermöglicht dynamische Dispatch
- Method Table (
fun-Array): Schneller Methoden-Aufruf (Virtual Table) - Hash-Feld: Optimiert Type-Switches (
O(1)stattO(n)Vergleich) - Caching: Die
itabwird einmal erstellt und gecacht - bei wiederholter Zuweisung wird die gleicheitabwiederverwendet
Bauen wir das Beispiel mit dem Fahrzeug etwas aus.
package main
import "fmt"
type Vehicle interface {
Drive(miles int)
}
type Car struct {
Vendor string
Miles int
}
func (c Car) Drive(miles int) {
c.Miles += miles
fmt.Printf("Fahrzeug fährt %d Meilen\n", miles)
}
func main() {
var v Vehicle
// v ist nil (kein Typ, kein Wert)
fmt.Printf("Typ: %T | Wert: %v | Ist nil: %t\n", v, v, v == nil)
// Nun bekommt "v" einen Wert vom Typ Car
v = Car{Vendor: "BMW", Miles: 0}
fmt.Printf("Typ: %T | Wert: %v | Ist nil: %t\n", v, v, v == nil)
// Nun können wir die Methode über "v" aufrufen
v.Drive(15)
}Typ: <nil> | Wert: <nil> | Ist nil: true
Typ: main.Car | Wert: {BMW 0} | Ist nil: false
Fahrzeug fährt 15 MeilenWas genau passierte hier?
- Zuerst ist
veinnil-Interfacetab=nildata=nil
- Nach
v = Car{Vendor: "BMW", Miles: 0}- Go sucht (oder erstellt) eine
itabfür die Kombination(Vehicle, Car) - Diese
itabenthält Pointer zu allen Methoden, dieCarimplementiert und dieVehicleerfordert - Der
Car-Wert wird auf dem Heap alloziert (wenn nötig) tabzeigt auf dieitabdatazeigt auf denCar-Wert
- Go sucht (oder erstellt) eine
Method Dispatch - Wie werden Methoden aufgerufen?
Wenn man v.Drive() aufruft, passiert intern (vereinfacht) Folgendes.
// 1. Hole die itab
tab := v.tab
// 2. Hole den Method-Pointer aus der "fun"-Tabelle
// Index "0" für die erste Methode (Drive)
methodPtr := tab.fun[0]
// 3. Rufe die Method mit dem "data"-Pointer auf
call methodPtr(v.data)Das ist der dynamic dispatch - die Methode wird zur Laufzeit basierend auf dem konkreten Typ bestimmt.
Memory-Layout - Wo liegt was?
Bei Value-Typ im Interface
v = Car{Vendor: "BMW", Miles: 0}Wenn der Wert klein genug ist (<= Pointer-Größe auf dem System), kann Go eine Optimierung vornehmen. Aber im Allgemeinen gilt Folgendes.
Stack / Heap:
+--------------------+
| s.tab ------+----> itab (global cached)
| s.data ------+----> +-------------------------+
+--------------------+ | Car struct |
| - Vendor: "BMW" |
| - Miles: 0 |
+-------------------------+
(auf dem Heap alloziert)Bei Pointer-Typ im Interface
v = &Car{Vendor: "BMW", Miles: 0}Wichtig: Bei Pointer-Typen speichert data den Pointer selbst, nicht den Wert.
Stack / Heap:
+--------------------+
| s.tab ------+----> itab für (*Car, Vehicle)
| s.data ------+----> +----------+ +-------------------+
+--------------------+ | *Car |----> | Car struct |
+----------+ | - Vendor: "BMW" |
| - Miles: 0 |
+-------------------+Performance-Implikationen
Was kostet ein Interface in welchen Situationen? Hier eine kurze Übersicht.
Heap-Allokation: Werte, die in Interfaces gespeichert werden, können auf den Heap “escapen” (Escape Analysis)
Indirektion: Jeder Methoden-Aufruf erfordert:
- Pointer-Dereferenzierung für
tab - Lookup in der
fun-Tabelle - Pointer-Dereferenzierung für
data
vtable-Lookup: Zwar schnell, aber langsamer als direkte Funktionsaufrufe
Schauen wir uns ein Beispiel an, wie wir eine Escape-Analysis durchführen können. Wir verwenden weiterhin unseren Fall mit dem Fahrzeug.
package main
type Vehicle interface {
Drive(miles int)
}
type Car struct {
Vendor string
Miles int
}
func (c Car) Drive(miles int) {
c.Miles += miles
}
func process() {
var v Vehicle
v = Car{Vendor: "BMW", Miles: 0}
v.Drive(15)
}Damit wir das Verhalten sehen können, müssen wir das Programm mit entsprechenden Flags kompilieren. Dazu führen wir im Ordner mit der main.go folgenden Befehl aus.
go build -gcflags="-m" main.goAls Ausgabe erhalten wir ca. Folgendes.
# command-line-arguments
./main.go:12:6: can inline Car.Drive
./main.go:16:6: can inline process
<autogenerated>:1: inlining call to Car.Drive
./main.go:12:7: c does not escape
// [!code highlight]
./main.go:18:9: Car{...} escapes to heap
# command-line-argumentsUns interessiert die markierte Zeile. Warum ist es hier so? Car-Wert muss länger leben als die Funktion (Interface kann ihn (den Wert) referenzieren), daher Heap-Allokation.
Beispiel - Interface-Internals sichtbar machen
Schauen wir uns nun an, wie wir mit einfachem Code uns die Interface-Internals “sichtbarer” machen können. Diese Herangehensweise hilft oft zum besseren Verständnis.
package main
import (
"fmt"
"unsafe"
)
type Vehicle interface {
Drive(miles int)
}
type Car struct {
Vendor string
Miles int
}
func (c Car) Drive(miles int) {
c.Miles += miles
fmt.Printf("Fahrzeug fährt %d Meilen", miles)
}
type iface struct {
tab uintptr
data uintptr
}
func main() {
var v Vehicle
// nil Interface
i := (*iface)(unsafe.Pointer(&v))
fmt.Printf("nil Interface: tab=%x, data=%x\n", i.tab, i.data)
// Interface mit Wert
v = Car{Vendor: "BMW", Miles: 0}
i = (*iface)(unsafe.Pointer(&v))
fmt.Printf("Mit Wert: tab=%x, data=%x\n", i.tab, i.data)
// Anderer Wert - gleicher Typ
v = Car{Vendor: "Audi", Miles: 230}
i2 := (*iface)(unsafe.Pointer(&v))
fmt.Printf("Anderer Wert: tab=%x, data=%x\n", i2.tab, i2.data)
fmt.Printf("Gleiche itab?: %v\n", i.tab == i2.tab)
}nil Interface: tab=0, data=0
Mit Wert: tab=1028197d8, data=102819750
Anderer Wert: tab=1028197d8, data=102819768
Gleiche itab?: trueWas sehen wir hier? Die itab Adresse ist gleich für beide Car Werte.
nil Interfaces und nil Werte
Dies ist eine häufige Fehlerquelle in Go. Welches Problem ist genau gemeint?
Ein Interface ist nur dann nil, wenn beide Komponenten nil sind (Typ und Wert).
Das nil Interface Problem verstehen
Ein Interface-Wert ist nur dann nil, wenn Folgendes gegeben ist.
tab == nil && data == nilWenn der Typ gesetzt ist (tab != nil), ist das Interface nicht nil, selbst wenn data == nil ist.
Verdeutlichen wir es an einem Beispiel.
package main
import "fmt"
type Vehicle interface {
Drive(miles int)
}
type Car struct {
Vendor string
Miles int
}
func (c *Car) Drive(miles int) {
if c == nil {
fmt.Println("Fahrzeug ist nil")
return
}
c.Miles += miles
fmt.Printf("Fahrzeug fährt %d Meilen", miles)
}
func main() {
// Das ist ein nil-Pointer
var c *Car = nil
var v Vehicle
v = c // Interface bekommt nil-Pointer
fmt.Printf("c == nil: %t\n", c == nil)
fmt.Printf("v == nil: %t\n", v == nil)
if v != nil {
v.Drive(15)
}
}c == nil: true
v == nil: false
Fahrzeug ist nilWas passiert hier intern?
cist einnil-Pointer vom Typ*Car- Typ:
*Car - Wert:
nil
- Typ:
v = cweist das Interface zu- Go erstellt eine
itabfür(Vehicle, Car) tab: Pointer auf dieitab(nicht nil)data:nil(kopiert vonc)
- Go erstellt eine
- Das Interface ist NICHT
nil, weiltab != nil v == nilprüft:tab == nil && data == nil=>false && true=>false
Ein grobes visuelles Schema davon.
Vor der Zuweisung:
var v Vehicle
+----------------------+
| tab: nil |
+----------------------+
| data: nil |
+----------------------+
v == nil --> trueNach: v = c (wobei c == nil, Typ: *Car)
+----------------------+
| tab: *itab ------+----> itab für (Vehicle, *Car)
+----------------------+
| data: nil |
+----------------------+
v == nil --> false (tab ist gesetzt)Direktes nil:
v = nil
+----------------------+
| tab: nil |
+----------------------+
| data: nil |
+----------------------+
v == nil --> trueWarum dieses Design?
Diese Design-Entscheidung hat Gründe.
1. Methoden-Aufrufe auf nil-Pointer sind erlaubt
var c *Car = nil
c.Drive(15) // Valide! Methode muss nil-Receiver handhaben2. Typ-Information bleibt erhalten
var v Vehicle = (*Car)(nil)
fmt.Printf("%T\n", v) // *main.Car (Typ ist bekannt)3. Reflection funktioniert
var v Vehicle = (*Car)(nil)
value := reflect.ValueOf(v)
fmt.Println(value.Kind()) // ptr (Typ-Info verfügbar)Häufige Bugs durch nil Interfaces
Bug: Funktion gibt typed nil zurück
Fehlerhafter Code
func getReader() io.Reader {
var f *os.File // nil
if someCondition {
f = openFile()
}
return f // BUG! Gibt non-nil Interface mit nil-Wert zurück
}
func main() {
r := getReader()
if r != nil { // true, selbst wenn f == nil
r.Read(buf) // PANIC wenn f == nil war
}
}Korrekter Code
func getReader() io.Reader {
var f *os.File // nil
if someCondition {
f = openFile()
}
if f == nil {
return nil // Explizit nil zurückgeben
}
return f
}Bug: Error-Handling
Fehlerhafter Code
func doSomething() error {
var err *MyError // nil
// ... Code ...
return err // BUG! error-Interface ist non-nil
}
func main() {
if err := doSomething(); err != nil {
// Wird IMMER ausgeführt, selbst wenn kein Fehler
log.Fatal(err)
}
}Korrekter Code
func doSomething() error {
var err *MyError // nil
// ... Code ...
if err != nil {
return err
}
return nil
}Bug: Interface-Variablen in Funktionen
Fehlerhafter Code
type Handler interface {
Handle()
}
func process(h Handler) {
if h == nil {
h = &DefaultHandler{}
}
h.Handle()
}
func main() {
var h *CustomHandler = nil
process(h) // BUG! h != nil im process()
}Korrekter Code
func main() {
var h Handler // Nicht *CustomHandler
if needsCustom {
h = &CustomHandler{}
}
process(h)
}Korrekte nil Interface Handhabung
Strategie 1 - nil Check vor Zuweisung
var v Vehicle
if c != nil {
v = c
}Strategie 2 - Explizite nil Rückgabe
func getVehicle() Vehicle {
var c *Car = nil
if c == nil {
return nil // Explizit nil zurückgeben
}
return c
}Strategie 3 - Methode prüft auf nil Receiver
func (c *Car) Drive(miles int) {
if c == nil {
fmt.Println("Keine Aktion möglich")
}
fmt.Printf("Fahrzeug fährt %d Meilen", miles)
}Strategie 4 - Reflection zur Prüfung
Wenn man wissen möchte, ob der Wert im Interface nil ist.
import "reflect"
func isNilValue(i interface{}) bool {
if i == nil {
return true
}
v := reflect.ValueOf(i)
switch v.Kind() {
case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
return v.IsNil()
}
return false
}
func main() {
var c *Car = nil
var v Vehicle = c
fmt.Println(v == nil)
fmt.Println(isNilValue(v))
}Type Assertions
Eine Type Assertion ermöglicht den Zugriff auf den konkreten Typ, der in einem Interface gespeichert ist.
Syntax
value := interfaceVariable.(ConcreteType)Die Type Assertion wird mit dem Typ in runden Klammern angewendet.
package main
import "fmt"
func main() {
var i interface{} = "Hallo"
s := i.(string) // Type Assertion
fmt.Println(s)
}HalloWas genau passiert hier?
ienthält einenstringi.(string)extrahiert denstringaus dem Interface
Würde der Typ nicht passen, würde Panic ausgelöst werden.
Type Assertion mit “ok” (idiomatisch)
Um Panics zu vermeiden, verwendet man das , ok Idiom. Was ist damit gemeint und wie sieht es aus?
Syntax
value, ok := interfaceVariable.(ConcreteType)Hier ist value der konvertierte Wert (oder Zero Value bei Fehler). In ok erhalten wir true, wenn der Typ passt, sonst false.
Schauen wir uns ein Beispiel an.
package main
import "fmt"
func main() {
var i interface{} = "Hallo"
// Richtige Type Assertion
if s, ok := i.(string); ok {
fmt.Println("String:", s)
} else {
fmt.Println("Kein String")
}
// False Type Assertion
if n, ok := i.(int); ok {
fmt.Println("Int:", n)
} else {
fmt.Println("Kein Int")
}
}String: Hallo
Kein IntMan sollte nach Möglichkeit immer diese Herangehensweise verwenden. Ausnahme könnte der Fall sein, wenn man sich 100% sicher ist, dass der Typ passt.
Type Switch - Typen prüfen
Wenn man verschiedene Typen unterschiedlich behandeln möchte, verwendet man einen Type Switch. Das gibt uns die Möglichkeit, die Typen zu prüfen und entsprechend dem jeweiligen Typ eine passende Aktion auszuführen.
Syntax
switch v := interfaceVariable.(type) {
case Type1:
// v ist vom Typ "Type1"
case Type2:
// v ist vom Typ "Type2"
default:
// v hat einen anderen Typ
}Verwenden wir ein klassisches und einfaches Beispiel dazu.
package main
import "fmt"
func describe(i any) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
case bool:
fmt.Printf("Boolean: %t\n", v)
case []int:
fmt.Printf("Integer-Slice: %v (Länge: %d)\n", v, len(v))
default:
fmt.Printf("Unbekannter Typ: %T\n", v)
}
}
func main() {
describe(42)
describe("Hallo")
describe(true)
describe([]int{1, 2, 3})
describe(3.14)
}Integer: 42
String: Hallo
Boolean: true
Integer-Slice: [1 2 3] (Länge: 3)
Unbekannter Typ: float64Die Verwendung von switch ist übersichtlicher, wenn man mehrere Typen prüfen muss, als für jeden Typ eine vollständige Assertion Prüfung mit ok durchzuführen. Weiterhin ist ein Vorteil, dass wir pro case Fall mehrere Typen prüfen können (int, int64).
Häufige Stolperfallen
Interface-Implementierung ist implizit — kein implements.
In Go gibt es kein Schlüsselwort, das einen Typ formal an ein Interface bindet. Sobald die Methodensignaturen passen, erfüllt der Typ das Interface. Das ist mächtig, aber auch tückisch: Eine kleine Signatur-Abweichung (Pointer- statt Wert-Receiver, anderer Rückgabetyp) lässt die Implementierung still durchfallen. Ein bewährter Trick ist die Compile-Time-Prüfung var _ MyInterface = (*MyType)(nil) — schlägt der Build fehl, fehlt eine Methode.
Die Nil-Interface-Falle: typed nil ist nicht nil.
Ein Interface ist nur dann == nil, wenn sowohl Typ-Pointer (tab) als auch Daten-Pointer (data) null sind. Wer einen var p *Car = nil einem Vehicle zuweist, bekommt ein Interface mit gesetztem Typ und nil-Daten — das Interface ist nicht nil, aber ein Methodenaufruf segfaultet, sobald die Methode auf den Receiver zugreift. Klassisches Symptom: if err != nil wird wahr, obwohl logisch kein Fehler vorliegt.
any ist nur ein Alias für interface{} (seit Go 1.18).
type any = interface{} — beide sind exakt derselbe Typ, nicht nur kompatibel. Du kannst sie frei mischen. Stilistisch ist any in neuem Code idiomatisch; interface{} siehst du in älterem Code und in der Standardbibliothek noch oft.
Type Assertion ohne , ok panischt bei Mismatch.
s := i.(string) löst eine Runtime-Panic aus, sobald i keinen string enthält — auch dann, wenn i selbst nil ist. Verwende immer das Comma-ok-Idiom s, ok := i.(string), außer du bist absolut sicher über den dynamischen Typ. Im Type Switch ist diese Sicherheit eingebaut.
Empty Interface verliert Typ-Info — Cast-Tax.
Wer any als Container nutzt, zahlt jedes Mal beim Auspacken: Type Assertion oder Type Switch sind Pflicht, bevor du wieder mit dem konkreten Typ arbeiten kannst. Generics (Go 1.18+) sind in vielen Fällen die bessere Wahl — Typ-Info bleibt erhalten und der Compiler hilft.
"Accept interfaces, return structs."
Idiomatisches Go: Funktionen nehmen kleine Interfaces als Parameter (Flexibilität für den Aufrufer), geben aber konkrete Typen zurück (klare Erwartung an den Empfänger). Ein Rückgabe-Interface zwingt jeden Aufrufer in Type Assertions, sobald er an Felder oder zusätzliche Methoden ran will.
Interface-Pollution: zu kleine oder zu große Interfaces.
Riesige Interfaces mit zwanzig Methoden sind in Go ein Geruch — sie zwingen jeden Implementierer zu allem. Das Go-Idiom geht den anderen Weg: kleine, fokussierte Interfaces (io.Reader, io.Writer, fmt.Stringer), bei Bedarf per Embedding kombiniert. Faustregel: ein Interface entsteht beim Konsumenten, nicht beim Produzenten.
Method-Set-Regel: Pointer-Receiver heißt Pointer-Wert nötig.
Hat eine Methode den Receiver *T, gehört sie nur zum Method Set von *T, nicht zu T. Ein Wert vom Typ T erfüllt das Interface dann nicht — du musst &t übergeben. Mischst du Wert- und Pointer-Receiver auf demselben Typ, droht zusätzlich Verwirrung über Mutationen. Konvention: alle Methoden eines Typs entweder durchgehend Wert- oder durchgehend Pointer-Receiver.
Weiterführende Ressourcen
Externe Quellen
- Interface types – Go Language Specification
- Type assertions – Go Specification
- Type switches – Go Specification
- Effective Go: Interfaces
- Go Code Review Comments: Interfaces