switch v := wert.(type) {
case int:
return v * 2
case string:
return strings.Repeat(v, 2)
...
Interfaces
Interfaces (Schnittstellen) sind eine Besonderheit in der Programmiersprache Go. Sie sind eine Art beschreibender Datentyp und gleichzeitig ein Platzhalter für irgend etwas. Beide Aspekte sollen hier getrennt beschrieben werden, obwohl sie streng genommen zwei Konsequenzen derselben Logik sind.
Interfaces sind ein sehr wichtiger Bestandteil von Go, sehr viele Funktionen in der Standardbibliothek benutzen sie. Dennoch sind sie für den Anwender nicht immer leicht zu verstehen, da sich in anderen Programmiersprachen keine direkte Entsprechung findet. In manchen anderen Programmiersprachen muss man explizit sagen, dass eine Funktion oder ein Datentyp ein Interface bedient, in Go passiert das automatisch. Es ist auch nicht leicht herauszufinden, welche Datentypen welche Interfaces implementieren.
Interfaces als Platzhalter für beliebige Datentypen
Wenn die Funktion fmt.Println()
aufgerufen wird, man beliebige Datentypen übergeben.
package main
import "fmt"
func beispiel(dummy int) {
// ohne Funktionalität
}
func main() {
fmt.Println("Hallo", beispiel, 1234)
}
Hier ist das erste Argument vom Typ string
, das zweite vom Typ func(int)
und das dritte vom Typ int. Wie funktioniert das in der Praxis?
Da man in einer Funktionsdefinition genau festlegen muss, welche Typen übergeben werden, warum hat fmt.Println()
hier eine Sonderstellung?
Wenn man sich unter https://golang.org/pkg/fmt/#Println die Funktionsbeschreibung anschaut, sieht man, dass das Argument ein Interface-Typ ist:
func Println(a ...interface{}) (n int, err error)
...
bedeutet, dass eine beliebige Anzahl Parameter übergeben werden kann.
Diese Parameter sind vom Typ interface{}
und können beliebige Inhalte haben.
Erst später wird überprüft, welcher Art ein bestimmtes Argument ist.
Als Beispiel soll eine Funktion erstellt werden, die ein Argument annimmt.
Ist es eine Zahl (int
), soll das doppelte der Zahl zurückgegeben werden.
Ist das Argument eine Zeichenkette (string
), soll sie zweimal hintereinander geschrieben zurückgegeben werden. In der Standardbibliothek im Paket strings
gibt es eine passende Funktion dafür.
Die erste naive Implementierung schlägt fehl:
package main
import "fmt"
func verdoppeln(wert interface{}) interface{} {
// Achtung, geht nicht:
return wert * 2
}
func main() {
fmt.Println(verdoppeln(4))
}
Die Funktionsdefinition verdoppeln
sieht richtig aus, es wird ein Wert vom Typ interface{}
erwartet und zurückgegeben.
Wie erwähnt, kann interface{}
einen beliebigen Datentyp speichern. Das Problem wird aber ersichtlich, wenn man das Programm laufen lässt:
./main.go:6: invalid operation: wert * 2 (mismatched types interface {} and int)
Der Go-Compiler sagt, dass er die Variable vom Typ interface{}
nicht mit einer Ganzzahl (int
) multiplizieren kann.
Hier muss eine Typzusicherung benutzt werden (engl. type assertion).
Eine Typzusicherung hat die Form variable.(Typ)
, in dem Beispiel heißt es dann:
func verdoppeln(wert interface{}) interface{} {
return wert.(int) * 2
}
Damit sichern das Programm dem Compiler zu: »in der Variablen wert
steckt eine Ganzzahl«.
Die Schwierigkeit ist, dass der wirkliche Typ nur zur Laufzeit bekannt ist.
Der Compiler kann nicht erkennen, mit welchem Wert die Funktion verdoppeln()
aufgerufen wird.
Was passiert also, wenn das folgendes Programm ausgeführt wird? (Bitte ausprobieren!)
package main
import "fmt"
func verdoppeln(wert interface{}) interface{} {
return wert.(int) * 2
}
func main() {
fmt.Println(verdoppeln("A"))
}
Hier ist der Datentyp des Arguments in Wirklichkeit eine Zeichenkette. Es wird aber dem Compiler zugesichert, dass es sich um eine Ganzzahl handelt. Das System erkennt aber erst zur Laufzeit das Problem und löst eine Ausnahmesituation (panic) aus:
panic: interface conversion: interface is string, not int
zusammen mit einem sogenannten stack trace, also einer Beschreibung der Funktionen, die aufgerufen wurden und zum eigentlichen Problem zeigen. Damit kann der Programmierer die problematische Stelle schnell finden und durch »scharfes hinsehen« meist den Fehler korrigieren.
Um nun zu unterscheiden, welcher Typ als Argument für die Funktion übergeben wurde, gibt es eine spezielle Syntax der Switch-Anweisung:
package main
import (
"fmt"
"strings"
)
func verdoppeln(wert interface{}) interface{} {
switch wert.(type) {
case int:
return wert.(int) * 2
case string:
return strings.Repeat(wert.(string), 2)
default:
fmt.Println("Konnte Typ von wert nicht ermitteln!")
return nil
}
}
func main() {
fmt.Println(verdoppeln(2))
fmt.Println(verdoppeln("A"))
// Typ float64, Fehler!
fmt.Println(verdoppeln(1.1))
}
In der Funktion verdoppeln()
wird nun der Typ von wert
abgefragt und je
nach Ergebnis die entsprechende Funktion aufgerufen.
Die Switch-Anweisung kann man auch etwas einfacher schreiben:
v
je nach Fall (case) schon den richtigen Typ und braucht nicht durch eine erneute Typzusicherung ermittelt werden.Interface als beschreibender Datentyp
Die zweite Variante[1] des Interface-Datentyps gibt an, welche Methoden in einer Struktur (struct
) definiert sein müssen, damit es diese Schnittstelle implementiert.
Als Beispiel sei noch einmal die Funktion fmt.Println()
genommen.
Die Argumente werden von Printf »schön« ausgegeben.
Das kann je nach Datentyp unterschiedlich sein.
Das funktioniert sogar mit eigenen Datentypen.
Dafür muss das Stringer Interface implementiert werden.
Was sich kompliziert anhört, ist ganz einfach.
Es muss auf dieser Datenstruktur die Funktion String() string
definiert werden, also eine Funktion, die eine Zeichenkette zurückgibt.
package main
import (
"fmt"
"strings"
)
type iban struct {
iban string
}
// String formatiert die IBAN lesbar mit Leerzeichen getrennt
func (i iban) String() string { ①
var ret []string
var pos int // initialisiert mit 0
l := 4 // Länge der einzelnen Abschnitte
for ; pos < len(i.iban); pos += l { ②
if pos+l > len(i.iban) { ③
l = len(i.iban) - pos
}
ret = append(ret, i.iban[pos:pos+l])
}
return strings.Join(ret, " ") ④
}
func main() {
konto := iban{"DE41500105170123456789"}
fmt.Println(konto) ⑤
}
①
Die Funktion String() implementiert das Interface fmt.Stringer
Damit wird sie von fmt.Println()
benutzt.
②
Nach jedem Schleifendurchlauf wird pos
erhöht. Die Schleife läuft solange, bis pos
nicht mehr kleiner als die Länge der iban
ist.
③
Wenn hinten nicht mehr als vier Stellen ausgegeben werden müssen, dann wird die Variable l
entsprechend verringert. Das würde sich auch auf den nächsten Schleifendurchlauf auswirken, in diesem Fall ist die Schleife aber beendet (die Bedingung wird nicht mehr erfüllt)
④
Die einzelnen Segmente sind als Einträge im Slice ret
gespeichert. Hier wird der Inhalt von ret
mit Leerzeichen getrennt zusammengeführt.
⑤
Die Funktion fmt.Println()
prüft intern, ob der Datentyp iban
eine Funktion String() string
definiert und ruft diese auf.
Interfaces sind hauptsächlich dann von Nutzen, wenn verschiedene Datentypen dieselben Methoden haben. Für kleinere Programme wird das wohl eher seltener zum Einsatz kommen.
Ich möchte an dieser Stelle zwei Beispiele geben, die die Nützlichkeit von Interfaces andeuten sollen.
Das eine ist die allgemeine Sortierroutine in der Standardbibliothek von Go (Paket sort
), das andere ist die Funktion io.Copy()
, die Daten aus beliebigen Quellen in beliebige Ziele kopiert.
Beispiel: Sortieren von Daten
Eine häufig vorkommende Situation in einem Programm ist die, dass Daten sortiert werden sollen.
Dazu stellt Go eine allgemeine Sortierroutine zur Verfügung.
Das Paket kann aber nicht für jede Datenstruktur wissen, nach welchen Kriterien sortiert werden muss und welche Konsistenzbedingungen erfüllt sein müssen, wenn zwei Daten vertauscht werden.
Daher muss der Anwender »nachhelfen« und die Methoden Len()
, Less()
und Swap()
implementieren.
Wenn auf einem Datentyp diese Methoden definiert sind, ist der Datentyp konform zur Schnittstelle sort.Interface
(mit großem i).
Und das ist genau der Datentyp, den die Funktion sort.Sort()
erwartet, um zu sortieren.
In diesem Beispiel wird ein Typ person
definiert und ein Typ besucher
, der ein Slice vom erstgenannten Typ ist:
package main
type besucher []person
type person struct {
alter int
name string
}
func main() {
var b besucher
b = []person{
{23, "Andreas"},
{37, "Marie"},
{27, "Peter"},
{27, "Karina"},
}
}
Um zu wissen, wie die genannten Funktionen aussehen sollen, muss man sich die Definition der Schnittstelle anschauen (https://golang.org/pkg/sort/#Interface):
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less returns whether the element with index i should sort
// before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
Die drei Methoden, die implementiert werden, müssen genau diese Signatur haben, also genau die Argumenttypen und Rückgabewerte.
package main
import (
"fmt"
"sort"
)
type besucher []person
type person struct {
alter int
name string
}
func (b besucher) Len() int {
return len(b)
}
func (b besucher) Less(i, j int) bool {
return b[i].alter < b[j].alter
}
func (b besucher) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
func main() {
var b besucher
b = []person{
{23, "Andreas"},
{37, "Marie"},
{27, "Peter"},
{27, "Karina"},
}
fmt.Println(b)
sort.Sort(b)
fmt.Println(b)
}
Da jetzt die drei geforderten Methoden für den Typ besucher
implementiert sind, kann sort.Sort()
auf unseren Typ angewendet werden.
Beispiel: Kopieren von Daten
Das zweite Beispiel taucht in der Praxis häufig auf. Es werden aus einer Quelle Daten eingelesen (z.B. Netzwerk, Benutzereingabe) und an anderer Stelle ausgegeben (Standard-Ausgabe, Datei). Dazu werden die Daten aus der Quelle gelesen und ins Ziel geschrieben.
Auch hier ist in der Standardbibliothek im Paket io
eine praktische Funktion
enthalten, die das Kopieren für uns übernimmt.
Die Funktion ist wie folgt definiert:
func Copy(dst Writer, src Reader) (written int64, err error)
Die Quelle (src
) muss vom Typ Reader
sein, das Ziel (dst
) vom Typ
Writer
. Es ist zu diesem Zeit unklar, wie genau die Interfaces aussehen
müssen. Ein erster Hinweis auf die zu implementierenden Methoden ist der Name
der Schnittstelle. Meist werden (englische) Verben mit er am Ende verwendet.
Die Schnittstelle Reader
erfordert also die Methode Read()
. Eine andere
Schnittstelle aus dem Paket io
heißt ReadCloser
. Dem Namen nach weiß man,
dass das Interface die Methoden Read()
und Close()
erfordert. Letztendlich
ist das aber nur eine Konvention, wie vieles in Go. Um sicher zu gehen muss
man sich die Dokumentation der Schnittstele anschauen. In dem Fall aus dem
Beispiel ist das im Paket io
das Interface Reader
, das wie folgt definiert
ist (https://golang.org/pkg/io/#Reader):
type Reader interface {
Read(p []byte) (n int, err error)
}
Writer hat diese Definition (https://golang.org/pkg/io/#Writer):
type Writer interface {
Write(p []byte) (n int, err error)
}
Das bedeutet in der Praxis, dass für unsere Quelle ein Datentyp Implementiert
werden muss, das die Methode Read()
wie oben beinhaltet und für das
Datenziel (Senke) ein Datentyp mit der entsprechenden Methode Write()
.
Praktischerweise bieten fast alle Schreib- und Leseoperationen in Go (genauer:
in der Standardbibliothek) diese Implementierung der Methoden Read()
und
Write()
und können damit in io.Copy()
benutzt werden.
Auch hier soll ein konkretes Beispiel zeigen, wie die Schnittstellen von
io.Copy()
genutzt werden können:
package main
import (
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("/etc/passwd")
defer file.Close()
if err != nil {
fmt.Println("Fehler aufgetreten:", err)
return
}
_, err = io.Copy(os.Stdout, file)
if err != nil {
fmt.Println("Fehler aufgetreten:", err)
return
}
}
Es wird mit os.Open()
ein os.File
-Objekt erzeugt.
Auf diesem Objekt ist die Methode Read(b []byte) (n int, err error)
definiert, die genau dem von io.Copy()
Interface Reader
entspricht.
os.Stdout
ist ein Objekt, das die Methode Write()
in der geforderten Ausprägung implementiert.
Dadurch kann nun io.Copy()
aufgerufen werden.
Intern benutzt io.Copy()
die Methode Read()
auf der Quelle und Write()
für das Ziel.
Es wird vom Compiler sichergestellt, dass die beiden Methoden auf den Objekten auch definiert sind.
Noch zwei Hinweise zu dem Programm oben: mit defer
wird eine Funktion für später vorgemerkt, sie wird ausgeführt, sobald die Funktion verlassen wird (z.B. nach einem return
).
Der Unterstrich (_
) in der Zeile mit io.Copy()
hat die Bedeutung, dass der Wert ignoriert wird und nur der zweite Rückgabewert (der Fehlerwert) von Bedeutung ist.