Programmiersprache: Generics in Go

Seite 3: Typ-Parameter bei Methoden

Inhaltsverzeichnis

Die Einführung von Typ-Parametern ist die fundamentale Erweiterung von Go, die generisches Programmieren ermöglichen wird. Sie werden bei Methoden in eckigen Klammern vor die Parameterliste geschrieben. Hier ein Codebeispiel:

// Print prints all elements of the given slice to stdout
func Print[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

func main() {
    // Call print with explicit type parameter
    Print[int]([]int{1, 2, 3})

    // Let go figure out the type parameter using type inference
    Print([]int{1, 2, 3})
}

Das Beispiel enthält einige Besonderheiten. In der Methodendeklaration von Print findet man ein neues Go-Sprachkonstrukt: any. Es steht für jeden beliebigen Datentyp, ist also eine kürzere Schreibweise des leeren Interface interface{}. any lässt sich nicht überall verwenden, wo bisher interface{} stand. Es ist nur in Verbindung mit Typ-Parametern verwendbar. Man wird sehen, ob sich das in Zukunft ändert.

Der zweite zu beachtende Punkt im Codebeispiel ist Type Inference beim Aufruf von Print. Während bei der ersten Aufrufvariante der Typ-Parameter int explizit angegeben wird, findet ihn Go in der zweiten Variante durch Ansehen des Typs der Methodenargumente automatisch heraus. Der aktuelle Designentwurf für generische Programmierung in Go enthält eine ausführliche Beschreibung aller Regeln für Type Inference. Aus Platzgründen beschränkt sich der Artikel auf die wesentlichen Punkte, die für den Großteil der praktischen Anwendungsfälle relevant sind.

Natürlich können generische Methoden auch mehrere Typ-Parameter verwenden. Das folgende Codebeispiel zeigt die klassische Map-Funktion, die ein Slice mit Werten von einem bestimmten Typ (hier int) mit einer Mapping-Funktion in ein Slice bestehend aus Werten eines anderen Typs (hier bool) umwandelt:

func Map[T1, T2 any](items []T1, mapFunc func(T1) T2) []T2 {
    result := make([]T2, len(items))
    for index, item := range items {
        result[index] = mapFunc(item)
    }
    return result
}

func main() {
    Print(Map([]int{1, 2, 3}, func(item int) bool { return item % 2 == 0 }))
}

Leserinnen und Leser, die Erfahrung mit Type Inference in Verbindung mit Generics und Lambda-Ausdrücken in anderen Programmiersprachen haben, werden am zuvor gezeigten Beispiel Einschränkungen von Go im Vergleich zu einer Sprache wie C# sehen. In Go sind die Typen für Parameter und Rückgabewerte bei anonymen Funktionen (im obigen Beispiel die anonyme Mapping-Funktion func(item int) bool { ... } im Aufruf von Map) anzugeben, während sie in C# der Compiler über Type Inference ergänzen würde.

Eine weitere Einschränkung im Vergleich zu C# ist, dass in Go Kovarianz oder Kontravarianz nicht vorgesehen ist. Einschränkungen wie diese spiegeln wider, dass in Go wie erwähnt die Einfachheit der Sprache und des Compilers höher bewertet werden als das Einsparen von ein paar Zeichen Code durch besonders kompakte Sprachkonstrukte.

Eine Besonderheit von Go sind Channels. Channels funktionieren auch in Verbindung mit generischer Programmierung und Typ-Parametern. Das folgende Codebeispiel zeigt eine einfache, generische Methode concat, mit der sich zwei Channels aneinanderhängen lassen:

func concat[T any](c1, c2 <-chan T) <-chan T {
    r := make(chan T)
    go func(c1, c2 <-chan T, r chan<- T) {
        defer close(r)
        for v := range c1 {
            r <- v
        }
        
        for v := range c2 {
            r <- v
        }
    
    }(c1, c2, r)
    return r
}

func main() {
    c1 := make(chan string, 2)
    c2 := make(chan string, 2)
        
    go func() {
        c1 <- "Hello"
        c1 <- ", "
        close(c1)
    
        c2 <- "World"
        c2 <- "!"
        close(c2)
    }()

    c3 := concat(c1, c2)
    for elem := range c3 {
        fmt.Print(elem) // Will result in "Hello, World!"
    }
}