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.
Links
- Inhaltsverzeichnis
- Vorheriger Abschnitt: Objektorientierte Programmierung
- Nächster Abschnitt: Nebenläufigkeit