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

Go
type InterfaceName interface {
    MethodName(parameter Typ) RückgabeTyp
    // Weitere Methoden ...
}

Hier ein sehr einfaches Beispiel.

Go 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.

Go Beispiel
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")
    }

}
Output
Credit Terminal zahlt Scheine aus: 2330.00
Coin Terminal zahl MÜNZEN aus: 200.00

Ok, 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

Go
type InterfaceName interface {
    Method1(param1 Type1, param2 Type2) ReturnType
    Method2() (ReturnType1, ReturnType2)
    Method3(param Type) error
}

Beispiel

Go
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

Go
// 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

Go
interface{}

Ab Go 1.18 gibt es den Alias any, der identisch zu interface{} ist.

Go
type any = interface{}

Schauen wir uns, wie man das Empty Interface verwenden kann.

Go Beispiel
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)
}
Output
42
Hallo
true
[1 2 3]

Im Grunde könnten wir auch folgendermaßen unsere i Variable definieren.

Go
var i any

Wann 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.

Go Beispiel
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"])
}
Output
John
30
true

Implizite 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.

Go Beispiel
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:

  1. Dynamischer Typ: Der konkrete Typ des gespeicherten Werts
  2. 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

Go
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

Go
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.

Go
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.

Go
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?

  1. Typ-Information und Wert getrennt: Ermöglicht dynamische Dispatch
  2. Method Table (fun-Array): Schneller Methoden-Aufruf (Virtual Table)
  3. Hash-Feld: Optimiert Type-Switches (O(1) statt O(n) Vergleich)
  4. Caching: Die itab wird einmal erstellt und gecacht - bei wiederholter Zuweisung wird die gleiche itab wiederverwendet

Bauen wir das Beispiel mit dem Fahrzeug etwas aus.

Go Beispiel
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)

}
Output
Typ: <nil> | Wert: <nil> | Ist nil: true
Typ: main.Car | Wert: {BMW 0} | Ist nil: false
Fahrzeug fährt 15 Meilen

Was genau passierte hier?

  1. Zuerst ist v ein nil-Interface
    • tab = nil
    • data = nil
  2. Nach v = Car{Vendor: "BMW", Miles: 0}
    • Go sucht (oder erstellt) eine itab für die Kombination (Vehicle, Car)
    • Diese itab enthält Pointer zu allen Methoden, die Car implementiert und die Vehicle erfordert
    • Der Car-Wert wird auf dem Heap alloziert (wenn nötig)
    • tab zeigt auf die itab
    • data zeigt auf den Car-Wert

Method Dispatch - Wie werden Methoden aufgerufen?

Wenn man v.Drive() aufruft, passiert intern (vereinfacht) Folgendes.

Go
// 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

Go
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.

Go
Stack / Heap:

+--------------------+
| s.tab        ------+----> itab (global cached)
| s.data       ------+----> +-------------------------+
+--------------------+      | Car struct              |
                            | - Vendor: "BMW"         |
                            | - Miles:  0             |
                            +-------------------------+
                            (auf dem Heap alloziert)

Bei Pointer-Typ im Interface

Go
v = &Car{Vendor: "BMW", Miles: 0}

Wichtig: Bei Pointer-Typen speichert data den Pointer selbst, nicht den Wert.

Go
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.

Go Beispiel
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
go build -gcflags="-m" main.go

Als Ausgabe erhalten wir ca. Folgendes.

Go
# 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-arguments

Uns 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.

Go Beispiel
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)

}
Output
nil Interface: tab=0, data=0
Mit Wert: tab=1028197d8, data=102819750
Anderer Wert: tab=1028197d8, data=102819768
Gleiche itab?: true

Was 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.

Go
tab == nil && data == nil

Wenn der Typ gesetzt ist (tab != nil), ist das Interface nicht nil, selbst wenn data == nil ist.

Verdeutlichen wir es an einem Beispiel.

Go 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)
    }
}
Output
c == nil: true
v == nil: false
Fahrzeug ist nil

Was passiert hier intern?

  1. c ist ein nil-Pointer vom Typ *Car
    • Typ: *Car
    • Wert: nil
  2. v = c weist das Interface zu
    • Go erstellt eine itab für (Vehicle, Car)
    • tab: Pointer auf die itab (nicht nil)
    • data: nil (kopiert von c)
  3. Das Interface ist NICHT nil, weil tab != nil
  4. v == nil prüft: tab == nil && data == nil => false && true => false

Ein grobes visuelles Schema davon.

Go
Vor der Zuweisung:
var v Vehicle

+----------------------+
| tab:   nil           |
+----------------------+
| data:  nil           |
+----------------------+

v == nil  --> true
Go
Nach: v = c        (wobei c == nil, Typ: *Car)

+----------------------+
| tab:   *itab   ------+----> itab für (Vehicle, *Car)
+----------------------+
| data:  nil           |
+----------------------+

v == nil  --> false   (tab ist gesetzt)
Go
Direktes nil:
v = nil

+----------------------+
| tab:   nil           |
+----------------------+
| data:  nil           |
+----------------------+

v == nil  --> true

Warum dieses Design?

Diese Design-Entscheidung hat Gründe.

1. Methoden-Aufrufe auf nil-Pointer sind erlaubt

Go
var c *Car = nil
c.Drive(15) // Valide! Methode muss nil-Receiver handhaben

2. Typ-Information bleibt erhalten

Go
var v Vehicle = (*Car)(nil)
fmt.Printf("%T\n", v) // *main.Car (Typ ist bekannt)

3. Reflection funktioniert

Go
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

Go
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

Go
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

Go
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

Go
func doSomething() error {
    var err *MyError  // nil

    // ... Code ...

    if err != nil {
        return err
    }

    return nil
}

Bug: Interface-Variablen in Funktionen

Fehlerhafter Code

Go
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

Go
func main() {
    var h Handler  // Nicht *CustomHandler
    if needsCustom {
        h = &CustomHandler{}
    }

    process(h)
}

Korrekte nil Interface Handhabung

Strategie 1 - nil Check vor Zuweisung

Go
var v Vehicle
if c != nil {
    v = c
}

Strategie 2 - Explizite nil Rückgabe

Go
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

Go
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.

Go
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

Go
value := interfaceVariable.(ConcreteType)

Die Type Assertion wird mit dem Typ in runden Klammern angewendet.

Go Beispiel
package main

import "fmt"

func main() {
    var i interface{} = "Hallo"

    s := i.(string)  // Type Assertion
    fmt.Println(s)
}
Output
Hallo

Was genau passiert hier?

  • i enthält einen string
  • i.(string) extrahiert den string aus 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

Go
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.

Go Beispiel
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")
    }

}
Output
String: Hallo
Kein Int

Man 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

Go
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.

Go Beispiel
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)
}
Output
Integer: 42
String: Hallo
Boolean: true
Integer-Slice: [1 2 3] (Länge: 3)
Unbekannter Typ: float64

Die 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

/ Weiter

Zurück zu Interfaces

Zur Übersicht