navigation Navigation


Inhaltsverzeichnis

Basics


Interfaces in Go sind ein zentrales Konzept, das es ermöglicht, flexible und wiederverwendbare Code-Strukturen zu erstellen. Sie definieren eine Menge von Methoden, die ein Typ implementieren muss, ohne die konkrete Implementierung vorzugeben. Dies fördert die Entkopplung von Komponenten und erleichtert die Wartung und Erweiterung von Anwendungen. In diesem Artikel werden die Grundlagen von Interfaces behandelt, einschließlich ihrer Deklaration, Implementierung und der Verwendung von Type Assertions, um Typen dynamisch zu überprüfen und zu konvertieren. Praktische Beispiele verdeutlichen, wie Interfaces in der Go-Programmierung effektiv eingesetzt werden können.

Inhaltsverzeichnis

    Einführung

    Ein Interface in Go ist ein Vertrag, der definiert, welche Methoden ein Typ haben muss. Es ist wichtig zu verstehen, dass Interfaces in Go nicht wie Klassen funktionieren - sie definieren nur das Verhalten (welche Methoden vorhanden sein müssen), nicht die Implementierung.

    Go verwendet implizite Implementierung. Das bedeutet:

    • Man muss nicht explizit sagen “dieser Typ implementiert jenes Interface”
    • Ein Typ implementiert automatisch ein Interface, wenn er alle geforderten Methoden besitzt

    Interface-Deklaration

    Ein Interface wird mit dem Schlüsselwort interface deklariert.

    type InterfaceName interface {
        MethodeNameOne(paramOne typeOne) returnTypeOne
        MethodNameTwo(paramTwo typeTwo) returnTypeTwo
    }

    Namenskonventionen

    • Interface-Namen enden oft mit “er” (z.B. Writer, Reader, Stringer)
    • Einige Interfaces haben nur eine Methode - das ist völlig normal und sogar erwünscht
    • Die Interfaces sollen klein gehalten werden - “The bigger the interface, the weaker the abstraction”
    Beispiel - Deklaration
    package main
    
    // Mit einer Methode
    type Speaker interface {
        Speak() string
    }
    
    // Mit mehreren Methoden
    type Animal interface {
        Speak() string
        Move() string
        Age() int
    }
    • Speakter ist ein Interface, das nur eine Methode Speak() erfordert
    • Animal ist ein komplexeres Interface mit drei Methoden
    • Jeder Typ, der diese Methoden implementiert, erfüllt automatisch das Interface

    Implizite Implementierung

    In Go muss man nicht, wie in anderen Programmiersprachen, explizit angeben, dass ein Interface implementiert werden soll.

    // Das ist in Go nicht zulässig
    type Dog implements Animal // ❌ Falsch

    Stattdessen implementiert ein Typ automatisch ein Interface, sobald er alle geforderten Methoden besitzt.

    Beispiel
    package main
    
    import "fmt"
    
    // 1. Interface definieren
    type Speaker interface {
        Speak() string
    }
    
    // 2. Verschiedene Typen erstellen
    type Dog struct {
        Name string
    }
    
    type Cat struct {
        Name string
    }
    
    type Robot struct {
        Name string
    }
    
    // 3. Methoden für jeden Typ implementieren
    func (d Dog) Speak() string {
        return "Wuff! Ich bin " + d.Name
    }
    
    func (c Cat) Speak() string {
        return "Miau! Ich bin " + c.Name
    }
    
    func (r Robot) Speak() string {
        return "Beep! Ich bin " + r.Name
    }
    
    // 4. Funktion, die das Interface verwendet
    func LetSpeak(s Speaker) {
        fmt.Println(s.Speak())
    }
    
    func main() {
    
        // 5. Verschiedene Typen erstellen
        dog := Dog{Name: "Rex"}
        cat := Cat{Name: "Mimi"}
        robot := Robot{Name: "R2D"}
    
        // 6. Alle können als Speaker behandelt werden
        LetSpeak(dog)
        LetSpeak(cat)
        LetSpeak(robot)
    
    }
    Wuff! Ich bin Rex
    Miau! Ich bin Mimi
    Beep! Ich bin R2D

    Bauen wir ein anderes Beispiel auf, das die grundlegende Verwendung von Interfaces verdeutlicht.

    Beispiel
    package main
    
    import "fmt"
    
    type Vehicle interface {
        Start() string
        Stop() string
    }
    
    type Car struct {
        Brand string
    }
    
    type Bicycle struct {
        Color string
    }
    
    // Methoden für Start für ein Fahrzeug
    func (c Car) Start() string {
        return c.Brand + " startet den Motor"
    }
    
    func (c Car) Stop() string {
        return c.Brand + " stoppt den Motor"
    }
    
    // Methode für Start/Stop für ein Rad
    func (b Bicycle) Start() string {
        return "Das Fahrrad in Farbe " + b.Color + " wird getreten"
    }
    
    func (b Bicycle) Stop() string {
        return "Das Fahrrad in Farbe " + b.Color + " hält an"
    }
    
    func useVehicle(v Vehicle) {
        fmt.Println("=== Fahrzeug benutzen ===")
        fmt.Println(v.Start())
        fmt.Println("... fahre ...")
        fmt.Println(v.Stop())
        fmt.Println()
    }
    
    func main() {
    
        car := Car{Brand: "Audi"}
        bike := Bicycle{Color: "blau"}
    
        UseVehicle(car)
        UseVehicle(bike)
    
    }
    === Fahrzeug benutzen ===
    Audi startet den Motor
    ... fahre ...
    Audi stoppt den Motor
    
    === Fahrzeug benutzen ===
    Das Fahrrad in Farbe blau wird getreten
    ... fahre ...
    Das Fahrrad in Farbe blau hält an

    Hier ein weiteres, einfacheres Beispiel, um implizite Implementierung von Interfaces in Go zu demonstrieren.

    In diesem Beispiel wird ein Interface Notifier erstellt. Dieses Interface ist dann erfüllt, wenn die ein Typ die Methode notify implementiert.

    Beispiel
    package main
    
    import "fmt"
    
    type Notifier interface {
        notify(msg string)
    }
    
    type Email struct {
        address string
    }
    func (e Email) notify(msg string) {
        fmt.Printf("[E-Mail an %s] %s\n", e.address, msg)
    }
    
    type SMS struct {
        number string
    }
    func (s SMS) notify(msg string) {
        fmt.Printf("[SMS an %s] %s\n", s.number, msg)
    }
    
    func SendMessage(n Notifier, msg string) {
        n.notify(msg)
    }
    
    func main() {
    
        var myEmail Email = Email{
            address: "one@mail.com"
        }
        var mySms SMS = SMS{
            number: "1923001923"
        }
    
        SendMessage(myEmail, "E-Mail Nachricht")
        SendMessage(mySms, "SMS Nachricht")
    
        // Als Sammlung
        notifiers := []Notifier{
            Email{address: "two@mail.com"},
            SMS{number: "8923490232"},
        }
    
        for n := range notifiers {
            SendMessage(n, "Gleiche Nachricht an alle")
        }
    
    }
    [E-Mail an one@mail.com] E-Mail Nachricht
    [SMS an 1923001923] SMS Nachricht
    [E-Mail an two@mail.com] Gleiche Nachricht an alle
    [SMS an 8923490232] Gleiche Nachricht an alle

    Im oberen Beispiel werden zwei Typen erzeugt: Email und SMS. Diese beiden Typen haben bei der Deklaration erstmal jeweils nur eine Eigenschaft. Drunter wird eine Methode notify erzeugt, welche durch (e Email) und (s SMS) als jeweilige Methode vom Typ Email und SMS definiert wird. Das bedeutet, dass jede Instanz diese Methode einfach aufrufen kann {instance}.notify().

    Weiter unten wird die Funktion SendMessage definiert, welche einfach einen Typ annimmt, welcher das Interface Notifier erfüllt, annimmt.

    Das bedeutet, dass dieser Funktion eigentlich eigal ist, was für ein konkreter Typ es ist. Dieser Typ soll lediglich das Interface Notifier implementieren.

    Man könnte somit auch die Methoden der Typen direkt aufrufen.

    Methoden direkt aufrufen
    func main() {
    
        var myEmail Email = Email{
            address: "one@mail.com"
        }
        var mySms SMS = SMS{
            number: "1923001923"
        }
    
        // --- Aus Beispiel vorher ---
        // SendMessage(myEmail, "E-Mail Nachricht")
        // SendMessage(mySms, "SMS Nachricht")
    
        myEmail.notify("E-Mail Nachricht")
        mySms.notify("SMS Nachricht")
    
        // ...
    
    }

    Dies würde genauso funktionieren. Die Funktion SendMessage aus dem Beispiel oben war einfach ein Container/Provider, der etwas, was die Methode notify() mit der gleichen Konfiguration hat, die es das Interface vorgibt. Wenn die Signatur und der Name passen, kann die Funktion SendMessage an dem übergebenen Typ die Methode notify() aufrufen.

    Leere Interfaces

    Ein leeres Interface (Empty Interface) interface{} ist ein Interface ohne Methoden. Da jeder Typ mindestens null Methoden hat, implementiert jeder Typ automatisch das Empty Interface.

    Moderne Alternative any

    Seit Go 1.18 gibt es den Typ-Alias any, der identisch mit interface{} ist.

    // Diese beiden sind identisch
    var x interface{}
    var y any

    Verwendungszwecke

    Das Empty Interface wird verwendet, wenn:

    • Man einen Wert beliebigen Typs speichern möchte
    • Man eine Funktion schreibt, die verschiedene Typen akzeptieren soll
    • Man mit unbekannten Datenstrukturen arbeitet (z.B. JSON)

    Beispiel - Einfaches leeres Interface

    Beispiel
    package main
    
    import "fmt"
    
    func PrintAnything(value interface{}) {
        fmt.Printf("Wert: %s | Typ: %T\n", value, value)
    }
    
    func main() {
    
        PrintAnything(42)
        PrintAnything("Hello")
    
    }
    Wert: 42 | Typ: int
    Wert: Hello | Typ: string

    Auch eigene, benutzerdefinierte Typen implementieren dieses Interface und könnten entsprechend mit von der PrintAnything() Funktion verarbeitet werden.

    Beispiel (Eigene Typen)
    package main
    
    import "fmt"
    
    type Person struct {
        name string
        age int
    }
    
    func PrintAnything(value interface{}) {
        fmt.Printf("Wert: %s | Typ: %T\n", value, value)
    }
    
    func main() {
        var p Person = Person{
            name: "John",
            age: 24,
        }
    
        PrintAnything(p)
    }
    Wert: {John 24} | Typ: main.Person

    Type Assertions

    Wenn man ein interface{} hat, weiß man nicht, was für ein Typ dahintersteckt. Type Assertions helfen einem herauszufinden, was es wirklich ist.

    Hier ein grundlegendes Beispiel, das aufzeigt, wie man Typen prüfen kann.

    Grundlegendes Beispiel
    package main
    
    import "fmt"
    
    func main() {
    
        var someType interface{} = "Go"
    
        // Prüfen, ob es String ist
        stringValue, stringCheck := someType.(string)
        if stringCheck {
            fmt.Printf("'%v' ist ein %T\n", stringValue, stringValue)
        } else {
            fmt.Println("Es ist kein String")
        }
    
        // Prüfenm ob es eine Zahl ist
        intValue, intCheck := someType.(int)
        if intCheck {
            fmt.Printf("'%v' ist ein %T\n", intValue, intValue)
        } else {
            fmt.Println("Es ist keine Zahl")
        }
    
    }
    'Go' ist ein string
    Es ist keine Zahl

    Die Konstruktion someType.(string) oder someType.(int) fragt jeweils den Typ ab.

    Es gibt zwei Werte zurück: der Wert (falls richtig) und ok (true/false). ok ist true, wenn der Typ stimmt, sonst false.

    Praktisches Beispiel
    package main
    
    import "fmt"
    
    func ProcessValue(value interface{}) {
        if stringValue, stringCheck := value.(string); stringCheck {
            fmt.Printf("String verarbeitet: '%s' (Länge: %d)\n", stringValue, len(stringValue))
            return
        }
    
        if numValue, numCheck := value.(int); numCheck {
            fmt.Printf("Zahl verarbeitet: %d (doppelt: %d)\n", numValue, numValue * 2)
            return
        }
    
        if boolValue, boolCheck := value.(bool); boolCheck {
            fmt.Printf("Boolean verarbeitet: %t\n", boolValue)
            return
        }
    
        // Nichts davon
        fmt.Printf("Unbekannter Typ: %T\n", value)
    }
    
    func main() {
        ProcessValue("Go")
        ProcessValue(42)
        ProcessValue(true)
        ProcessValue(3.14)
    }

    Eine weitere Möglichkeit, Typen zu überprüfen stellt der Einsatz von switch dar. Damit kann man eleganten Code aufbauen, welcher je nach Typ bestimmte Aktionen ausführt.

    Beispiel mit switch
    package main
    
    import "fmt"
    
    func ProcessValue(value interface{}) {
        switch v := value.(type) {
            case string:
                fmt.Printf("String: '%s' (Länge: %d)\n", v, len(v))
            case int:
                fmt.Printf("Integer: %d (doppelt: %d)\n", v, v*2)
            case bool:
                fmt.Printf("Boolean: %t\n", v)
            default:
                fmt.Printf("Unbekannter Typ: %T mit Wert %v\n", v, v)
        }
    }
    
    func main() {
        values := []interface{}{
            "Go",
            42,
            true,
            3.14,
        }
    
        for _, value := range values {
            ProcessValue(value)
        }
    }
    String: 'Go' (Länge: 2)
    Zahl: 42 (doppelt: 84)
    Boolean: true
    Unbekannter Typ: float64 mit Wert 3.14