EMZETT.
Login

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

Kapitel 7 von 11 im Kurs Go. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

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.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")
}

Ausgabe:

Wuff bringt Stöckchen
kein Katze
Hund: bringt Stöckchen
Katze
21
direkt nach Stdout

Hinweis

Das leere Interface any (früher interface{}) 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 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)
}

Ausgabe:

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)

Übungsaufgabe

Definiere ein Interface Zahlung mit Betrag() float64 und zwei Typen. Schreibe eine generische Funktion Summe über eine Liste von Zahlungen.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Structs, Methoden und Zeiger

Weiter: Goroutinen, Channels und Synchronisation

Alle Kapitel: Go im Überblick