Kurz: Ein Interface beschreibt Methoden. Jeder Typ, der diese Methoden hat, erfüllt das Interface, ohne es zu deklarieren (strukturelle Typisierung, “Duck Typing” mit Compiler-Prüfung):
Teil des Kurses Go
Interfaces
Ein Interface beschreibt Methoden. Jeder Typ, der diese Methoden hat, erfüllt das Interface, ohne es zu deklarieren (strukturelle Typisierung, “Duck Typing” mit Compiler-Prüfung):
package main
import (
"fmt"
"math"
)
type Form interface {
Flaeche() float64
Umfang() float64
}
type Kreis struct{ R float64 }
type Rechteck struct{ B, H float64 }
func (k Kreis) Flaeche() float64 { return math.Pi * k.R * k.R }
func (k Kreis) Umfang() float64 { return 2 * math.Pi * k.R }
func (r Rechteck) Flaeche() float64 { return r.B * r.H }
func (r Rechteck) Umfang() float64 { return 2 * (r.B + r.H) }
func beschreibe(f Form) {
fmt.Printf("%T: Fläche %.2f, Umfang %.2f\n", f, f.Flaeche(), f.Umfang())
}
func main() {
formen := []Form{Kreis{1}, Rechteck{3, 4}}
for _, f := range formen {
beschreibe(f)
}
}Ausgabe:
main.Kreis: Fläche 3.14, Umfang 6.28
main.Rechteck: Fläche 12.00, Umfang 14.00Kleine Interfaces sind besser: Die Standardbibliothek lebt von Ein-Methoden-Interfaces wie io.Reader, io.Writer, fmt.Stringer, error, sort.Interface.
Typzusicherung und Typ-Switch
package main
import (
"fmt"
"io"
"os"
"strings"
)
type Tier interface{ Laut() string }
type Hund struct{}
type Katze struct{}
func (Hund) Laut() string { return "Wuff" }
func (Katze) Laut() string { return "Miau" }
func (Hund) Apportieren() string { return "bringt Stöckchen" }
func main() {
var t Tier = Hund{}
if h, ok := t.(Hund); ok { // Typzusicherung
fmt.Println(h.Laut(), h.Apportieren())
}
if _, ok := t.(Katze); !ok {
fmt.Println("kein Katze")
}
for _, x := range []Tier{Hund{}, Katze{}} {
switch v := x.(type) {
case Hund:
fmt.Println("Hund:", v.Apportieren())
case Katze:
fmt.Println("Katze")
}
}
// io.Reader / io.Writer: derselbe Code für Dateien, Netzwerk, Strings ...
var r io.Reader = strings.NewReader("Text aus einem Reader")
daten, _ := io.ReadAll(r)
fmt.Println(len(daten))
io.WriteString(os.Stdout, "direkt nach Stdout\n")
}Ausgabe:
Wuff bringt Stöckchen
kein Katze
Hund: bringt Stöckchen
Katze
21
direkt nach StdoutHinweis
Das leere Interface
any(früherinterface{}) nimmt jeden Wert auf. Nutze es sparsam: Du verlierst die Typprüfung.
Interface-Werte und nil
Ein Interface-Wert besteht aus (Typ, Wert). Ein Interface ist nur nil, wenn beides leer ist. Das führt zur bekannten Falle:
package main
import "fmt"
type MeinFehler struct{}
func (*MeinFehler) Error() string { return "fehler" }
func falsch() error {
var e *MeinFehler = nil
return e // Interface enthält (Typ *MeinFehler, Wert nil) -> nicht nil!
}
func richtig() error {
return nil
}
func main() {
fmt.Println(falsch() == nil, richtig() == nil)
}Ausgabe:
false trueGib bei Erfolg immer ausdrücklich nil zurück, nicht einen typisierten nil-Zeiger.
Generics (Typparameter)
Seit Go 1.18 gibt es Generics:
package main
import (
"cmp"
"fmt"
)
type Zahl interface {
~int | ~int64 | ~float64
}
func Summe[T Zahl](werte []T) T {
var s T
for _, w := range werte {
s += w
}
return s
}
func Map[T, U any](liste []T, f func(T) U) []U {
ergebnis := make([]U, 0, len(liste))
for _, x := range liste {
ergebnis = append(ergebnis, f(x))
}
return ergebnis
}
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
type Stapel[T any] struct{ daten []T }
func (s *Stapel[T]) Push(x T) { s.daten = append(s.daten, x) }
func (s *Stapel[T]) Pop() (T, bool) {
var null T
if len(s.daten) == 0 {
return null, false
}
x := s.daten[len(s.daten)-1]
s.daten = s.daten[:len(s.daten)-1]
return x, true
}
func main() {
fmt.Println(Summe([]int{1, 2, 3}), Summe([]float64{1.5, 2.5}))
fmt.Println(Map([]int{1, 2, 3}, func(x int) string { return fmt.Sprint(x * x) }))
fmt.Println(Max(3, 9), Max("kiwi", "apfel"))
s := &Stapel[string]{}
s.Push("a")
s.Push("b")
x, _ := s.Pop()
fmt.Println(x)
}Ausgabe:
6 4
[1 4 9]
9 kiwi
bany erlaubt jeden Typ, comparable Typen mit ==, cmp.Ordered Typen mit <. Mit ~int sind auch eigene Typen auf Basis von int erlaubt. Nutze Generics für Container und Hilfsfunktionen, aber nicht überall: Oft genügt ein Interface.
Merke
- Interfaces sind Methodenmengen; Typen erfüllen sie implizit
- Kleine Interfaces (
io.Reader,error,fmt.Stringer) sind am besten - Typzusicherung
x.(T)und Typ-Switch prüfen den konkreten Typ - Ein Interface mit typisiertem nil-Zeiger ist nicht
nil - Generics:
func F[T any](x T), mit Einschränkungen (cmp.Ordered, eigene Interfaces)
Übungsaufgabe
Definiere ein Interface Zahlung mit Betrag() float64 und zwei Typen. Schreibe eine generische Funktion Summe über eine Liste von Zahlungen.
Quiz zur Selbstkontrolle
Wie erfüllt ein Typ in Go ein Interface?
- Indem er alle Methoden des Interfaces besitzt, ohne es zu deklarieren (richtig)
- Mit implements
- Mit extends
- Durch Vererbung
Wofür steht any?
- Ein Typ, der jeden Wert aufnimmt (interface{}) (richtig)
- Ein Zahlentyp
- Ein Fehlertyp
- Ein Zeiger
Was ist die nil-Falle bei Interfaces?
- Ein Interface mit typisiertem nil-Zeiger ist selbst nicht nil (richtig)
- nil ist verboten
- nil stürzt immer ab
- Interfaces können nicht nil sein
Weiter im Kurs
Zurück: Structs, Methoden und Zeiger
Weiter: Goroutinen, Channels und Synchronisation
Alle Kapitel: Go im Überblick