2.6 Interfaces

Interface

Eines der besten Sprachmerkmale von Go sind Interfaces. Nach dem Lesen dieses Abschnitts wirst Du über dessen Implementation staunen.

Was ist ein Interface

Kurz gesagt, ein Interface dient der Zusammenfassung von Funktionen, die durch ihren ähnlichen Aufbau eine Beziehung zueinander haben.

Wie im Beispiel aus dem letzten Abschnitt haben Student und Mitarbeiter hier beide die Methode SagHallo(), welche sich ähnlich, aber nicht gleich verhalten.

Machen wir uns wieder an die Arbeit, indem wir beiden Structs die Methode Singen() hinzufügen. Zudem erweitern wir Student um die Methode GeldLeihen() und Mitarbeiter um die Methode GehaltAusgeben().

Nun besitzt Student die drei Methoden SagHallo(), Singen() und GeldLeihen() und Mitarbeiter SagHallo(), Singen() sowie GehaltAusgeben().

Diese Kombination von Methoden wird Interface genannt und umfasst sowohl Student als auch Mitarbeiter. Somit besitzen beide Datentypen die Interfaces SagHallo() und Singen(). Jedoch implementieren beide Datentypen keine gemeinsames Interfaces für GeldLeihen() und GehaltAusgeben(), da diese Methoden nicht für beide Structs definiert wurden.

Interface als Datentyp

Ein Interface definiert eine Liste von Methoden. Besitzt ein Datentyp alle Methoden, die das Interface definiert, so umfasst das Interface diesen Datentypen. Schauen wir uns ein Beispiel zur Verdeutlichung an.

type Mensch struct {
    name    string
    alter   int
    telefon string
}

type Student struct {
    Mensch
    schule string
    kredit float32
}

type Mitarbeiter struct {
    Mensch
    unternehmen string
    geld        float32
}

func (m *Mensch) SagHallo() {
    fmt.Printf("Hallo, ich bin %s und Du erreichst mich unter %s\n", m.name, m.telefon)
}

func (m *Mensch) Singen(liedtext string) {
    fmt.Println("La la, la la la, la la la la la...", liedtext)
}

func (m *Mensch) SichBetrinken(bierkrug string) {
    fmt.Println("schluck schluck schluck...", bierkrug)
}

// Mitarbeiter "überlädt" SagHallo
func (m *Mitarbeiter) SagHallo() {
    fmt.Printf("Hallo, ich bin %s und arbeite bei %s. Ruf mich unter der Nummer %s an\n", m.name,
        m.unternehmen, m.telefon) // Du kannst die Argumente auch auf zwei Zweilen aufteilen.
}

func (s *Student) GeldLeihen(betrag float32) {
    s.kredit += betrag // (immer und immer wieder...)
}

func (m *Mitarbeiter) GehaltAusgeben(betrag float32) {
    m.geld -= betrag // Einen Vodka bitte!!! Bring mich durch den Tag!
}

// Das Interface definieren
type Männer interface {
    SagHallo()
    Singe(liedtext string)
    SichBetrinken(bierkrug string)
}

type JungerMann interface {
    SagHallo()
    Singe(liedtext string)
    GeldLeihen(betrag float32)
}

type Greis interface {
    SagHallo()
    Singe(liedtext string)
    GeldAusgeben(betrag float32)
}

Wir wissen, dass ein Interface von jedem Datentypen implementiert werden und ein Datentyp viele Interfaces umfassen kann.

Zudem implementiert jeder Datentyp das leere Interface interface{}, da es keine Methoden definiert und alle Datentypen von Beginn an keine Methoden besitzen.

Interface als Datentyp

Welche Arten von Werten können mit einem Interface verknüpft werden? Wen wir eine Variable vom Typ Interface definieren, dann kann jeder Datentyp, der das Interface implementiert, der Variable zugewiesen werden.

Es ist wie im oberen Beispiel. Erstellen wir eine Variable "m" mit dem Interface Männer, kann jeder Student, Mensch oder Mitarbeiter "m" zugewiesen werden. So könnten wir ein Slice mit dem Interface Männer jeden Datentyp hinzufügen, der ebenfalls das Interface Männer implementiert. Bedenke aber, dass sich das Verhalten von Slices ändert, wenn dies Elemente eines Interface statt eines Datentypes verwendet.

package main

import "fmt"

type Mensch struct {
    name    string
    alter   int
    telefon string
}

type Student struct {
    Mensch
    schule string
    geld   float32
}

type Mitarbeiter struct {
    Mensch
    unternehmen string
    geld        float32
}

func (m Mensch) SagHallo() {
    fmt.Printf("Hallo, ich bin %s und Du erreicht mich unter %s\n", m.name, m.telefon)
}

func (m Mensch) Singe(liedtext string) {
    fmt.Println("La la la la...", liedtext)
}

func (m Mitarbeiter) SagHallo() {
    fmt.Printf("Hallo, ich bin %s und arbeite bei %s. Rufe mich unter der Nummer %s an\n", m.name,
        m.unternehmen, m.telefon) // Du kannst die Argumente auch auf zwei Zweilen aufteilen.
}

// Das Interface Männer wird von Mensch, Student und Mitarbeiter implementiert
type Männer interface {
    SagHallo()
    Singe(liedtext string)
}

func main() {
    mike := Student{Mensch{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    paul := Student{Mensch{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    sam := Mitarbeiter{Mensch{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    tom := Mitarbeiter{Mensch{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}

    // Definiere i vom Typ Interface Männer
    var i Männer

    // i kann Studenten zugewiesen bekommen
    i = mike
    fmt.Println("Das ist Mike, ein Student:")
    i.SagHallo()
    i.Singe("November rain")

    // i kann auch Mitarbeiter zugewiesen bekommen
    i = tom
    fmt.Println("Das ist Tom, ein Mitarbeiter:")
    i.SagHallo()
    i.Singe("Born to be wild")

    // Slice mit Männern
    fmt.Println("Nutzen wir einen Slice vom Typ Männer und schauen, was passiert")
    x := make([]Männer, 3)
    // Alle drei Variablen haben verschiedene Datentypen, implentieren aber das selbe Interface
    x[0], x[1], x[2] = paul, sam, mike

    for _, wert := range x {
        wert.SagHallo()
    }
}

Ein Interface ist eine Ansammlung von abstrakten Methoden, das jeder Datentypen implementieren kann, die noch nicht Teil des Interfaces sind. Daher kann es sich nicht selbst implemetieren

Leeres Interface

Ein leeres Interfaces umfasst keine Methoden, sodass alle Datentypen dieses Interface implementieren. Dies ist sehr nützlich, wenn wir irgendwann alle Datentypen speichern möchten. Es ist void* aus C sehr ähnlich.

// Definition eines leeren Interfaces
var a interface{}
var i int = 5
s := "Hallo Welt"
// a kann jeder Datentyp zugewiesen werden
a = i
a = s

Wenn eine Funktion ein leeres Interface als Argumenttyp verwendet, wird jeder Datentyp akzeptiert. Gleiches gilt für den Rückgabewert einer Funktion.

Ein Interface als Methodenargument

Jede Variable kann mit einem Interface genutzt werden. Aber wie können wir diese Eigenschaft nutzen, um einen beliebigen Datentyp einer Funktion zu übergeben?

Zum Beispiel nutzen wir fmt.Println sehr oft, aber hast Du jemals gemerkt, dass jeder Datentyp verwendet werden kann? Werfen wir mal einen Blick auf den open-source Code von fmt. Wir sehen die folgende Definition der Funktion.

type Stringer interface {
        String() string
}

Dies bedeutet, dass jeder Datentyp, der das Interface Stringer implementiert, fmt.Println übergeben werden kann. Beweisen wir es.

package main

import (
    "fmt"
    "strconv"
)

type Mensch struct {
    name    string
    alter   int
    telefon string
}

// Mensch implementiert fmt.Stringer
func (m Mensch) String() string {
    return "Name:" + m.name + ", Alter:" + strconv.Itoa(m.alter) + " Jahre, Kontakt:" + m.telefon
}

func main() {
    Bob := Mensch{"Bob", 39, "000-7777-XXX"}
    fmt.Println("Dieser Mensch ist: ", Bob)
}

Werfen wir nochmal einen Blick auf das Beispiel mit den Boxen von vorhin. Du wirst feststellen, dass der Datentyp Farbe ebenfalls das Interface Stringer definiert, sodass wir die Ausgabe formatieren können. Würden wir dies nicht tun, nutzt fmt.Println() die Standardformatierung.

fmt.Println("Die größte Box ist", boxen.HöchsteFarbe().String())
fmt.Println("Die größte Box ist", boxen.HöchsteFarbe())

Achtung: Wenn Du das Interface error implementierst, wird fmt error() ausrufen, sodass Du ab hier Stringer noch nicht definieren brauchst.

Datentyp eines Interfaces bestimmen

Wir wissen, dass einer Variable jeder Datentyp zugewiesen werden kann, der ein Interface mit dem originalen Datentypen teilt. Nun stellt sich die Frage, wie wir den genauen Datentypen einer Variable bestimmen können. Hierfür gibt es zwei Wege, die ich Dir zeigen möchte.

  • Überprüfung nach dem Komma-ok-Muster

Der in Go übliche Syntax lautet value, ok := element.(T). Er überprüft, ob eine Variable vom erwarteten Datentypen ist. "value" ist der Wert der Variable,"ok" ist vom Typ Boolean, "element" ist eine Interfacevariable und T der zu überprüfende Datentyp.

Wenn das Element dem erwarteten Datentypen entspricht, wird ok auf true gesetzt. Anderfalls ist er false.

Veranschaulichen wir dies anhand eines Beispiels.

package main

import (
    "fmt"
    "strconv"
)

type Element interface{}
type Liste []Element

type Person struct {
    name   string
    alter  int
}

func (p Person) String() string {
    return "(Name: " + p.name + " - Alter:     " + strconv.Itoa(p.alter) + " Jahre)"
}

func main() {
    liste := make(Liste, 3)
    liste[0] = 1       // ein Integer
    liste[1] = "Hallo" // ein String
    liste[2] = Person{"Dennis", 70}

    for index, element := range liste {
        if value, ok := element.(int); ok {
            fmt.Printf("liste[%d] ist ein Integer mit dem Wert %d\n", index, value)
        } else if value, ok := element.(string); ok {
            fmt.Printf("liste[%d] ist ein String mit dem Wert %s\n", index, value)
        } else if value, ok := element.(Person); ok {
            fmt.Printf("liste[%d] ist eine Person mit dem Wert %s\n", index, value)
        } else {
            fmt.Printf("liste[%d] hat einen anderen Datentyp\n", index)
        }
    }
}

Dieses Muster ist sehr einfach anzuwenden, aber wenn wir viele Datentypen zu bestimmen haben, sollten wir besser switch benutzen.

  • Überprüfung mit switch

Machen wir von switch gebrauch und schreiben unser Beispiel um.

package main

import (
    "fmt"
    "strconv"
)

type Element interface{}
type Liste []Element

type Person struct {
    name   string
    alter  int
}

func (p Person) String() string {
    return "(Name: " + p.name + " - Alter: " + strconv.Itoa(p.alter) + " Jahre)"
}

func main() {
    liste := make(Liste, 3)
    liste[0] = 1       // Ein Integer
    liste[1] = "Hello" // Ein String
    liste[2] = Person{"Dennis", 70}

    for index, element := range liste {
        switch value := element.(type) {
        case int:
            fmt.Printf("liste[%d] ein Integer mit dem Wert %d\n", index, value)
        case string:
            fmt.Printf("liste[%d] ist ein String mit dem Wert %s\n", index, value)
        case Person:
            fmt.Printf("liste[%d] ist eine Person mit dem Wert %s\n", index, value)
        default:
            fmt.Println("liste[%d] hat einen anderen Datentyp", index)
        }
    }
}

Eine Sache, die Du bedenken solltest, ist, dass element.(type) nur in Kombination mit switch genutzt werden kann. Andernfalls musst Du auf das Komma-ok-Muster zurückgreifen.

Eingebettete Interfaces

Eine der schönsten Eigenschaften von Go ist dessen eingebaute und vorrausschauende Syntax, etwa namenlose Eigentschaften in Structs. Nicht überraschend können wir dies ebenfalls mit Interfaces tun, die eingebettete Interfaces genannt werden. Auch hier gelten die selben Regeln wie bei namenlosen Eigenschaften. Anders ausgedrückt: wenn ein Interface ein anderes Interface einbettet, werden auch alle Methoden mit übernommen.

Im Quellcode des Pakets container/heap lässt sich folgende Definition finden:

type Interface interface {
        sort.Interface      // Eingetettetes sort.Interface
        Push(x interface{}) // Eine Push Methode, um Objekte im Heap zu speichern
        Pop() interface{}   // Eine Pop Methode, um Elemente aus dem heap zu löschen
}

Wie wir sehen können, handelt es sich bei sort.Interface um ein eingebettes Interface. Es beinhaltet neben Push() und Pop() die folgenden drei Methoden implizit.

type Interface interface {
        // Len gibt die Anzahl der Objekte in der Datenstruktur an.
        Len() int
        // Less gibt in Form eines Boolean an, ob i mit j getauscht werden sollte
        Less(i, j int) bool
        // Swap vertauscht die Elemente i und j.
        Swap(i, j int)
}

Ein weiteres Beispiel ist io.ReadWriter aus dem Paket io.

// io.ReadWriter
type ReadWriter interface {
        Reader
        Writer
}

Reflexion

Reflexion in Go wird genutzt, um Informationen während der Laufzeit zu bestimmen. Wir nutzen dafür das reflect Paket. Der offizelle Artikel erklärt die Funktionsweise von reflect in Go.

Die Nutzung von reflect umfasst drei Schritte. Als Erstes müssen wir ein Interface in reflect-Datentypen umwandeln (entweder in reflect.Type oder reflect.Value, aber dies ist Situationsabhängig).

t := reflect.TypeOf(i)    // Speichert den Datentyp von i in t und erlaubt den Zugriff auf alle Elemente
v := reflect.ValueOf(i)   // Erhalte den aktuellen Wert von i. Nutze v um den Wert zu ändern

Danach können wir die reflektierten Datentypen konvertieren, um ihre Werte zu erhalten.

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("Datentyp:", v.Type())
fmt.Println("Die Variante ist float64:", v.Kind() == reflect.Float64)
fmt.Println("Wert:", v.Float())

Wollen wir schließlich den Wert eines reflektierten Datentypen ändern, müssen wir ihn dynamisch machen. Wie vorhin angesprochen, gibt es einen Unterschied, wenn wir einen Wert als Kopie oder dessen Zeiger übergeben. Das untere Beispiel ist nicht kompilierbar.

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

Stattdessen müssen wir den folgenden Code verwenden, um die Werte der reflektierten Datentypen zu ändern.

var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

Nun kennen wir die Grundlagen der Reflexion. Es erfordert jedoch noch ein wenig Übung, um sich mit diesem Konzept vertraut zu machen.

results matching ""

    No results matching ""