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)
}
}main.Kreis: Fläche 3.14, Umfang 6.28 main.Rechteck: Fläche 12.00, Umfang 14.00
Kleine 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")
}Wuff bringt Stöckchen kein Katze Hund: bringt Stöckchen Katze 21 direkt nach Stdout
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)
}false true
Gib 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)
}6 4 [1 4 9] 9 kiwi b
any 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)
Aufgabe
Definiere ein Interface Zahlung mit Betrag() float64 und zwei Typen. Schreibe eine generische Funktion Summe über eine Liste von Zahlungen.