Programmiersprache: Generics in Go
Seite 5: Generische Typen
Neben generischen Funktionen wird man in Go generische Typen erstellen können. Die Syntax ist ähnlich wie bei den Funktionen: Zur Typ-Definition ergänzt man ein oder mehrere Typ-Parameter in eckigen Klammern.
Das folgende Codebeispiel erstellt einen einfachen, generischen Vektor-Datentyp. Die Vektor-Elemente werden mit einer Typ-Liste im Constraint vectorElement limitiert. Methoden des generischen Typs wie add können den Typ-Parameter verwenden, aber keine zusätzlichen Typ-Argumente hinzufügen.
type vectorElement interface {
type int, uint, float32, float64
}
type vector2d[T vectorElement] struct {
X T
Y T
}
func newVector2d[T vectorElement](x T, y T) vector2d[T] {
return vector2d[T]{X: x, Y: y};
}
func (v1 vector2d[T]) add(v2 vector2d[T]) vector2d[T] {
return vector2d[T]{X: v1.X + v2.X, Y: v1.Y + v2.Y}
}
func main() {
v1 := newVector2d(10, 10)
v2 := newVector2d(11, 11)
v3 := v1.add(v2)
fmt.Println(v3)
}
Keine generischen Interfaces
Entwicklerinnen und Entwickler, die generische Programmierung von anderen Sprachen wie Java, C# oder TypeScript kennen, wĂĽrden in Go wahrscheinlich generische Interfaces erwarten. An der Stelle sei daher darauf hingewiesen, dass sich Interfaces in Go nur fĂĽr Constraints verwenden lassen. Generische Interfaces wurden in diesem Artikel nicht vergessen, sie sind in Go nur aktuell nicht vorgesehen.
Diese Einschränkung hat Auswirkungen auf das Design von APIs. Andere Sprachen setzen beispielsweise generische Interfaces für Iteratoren und darauf aufbauende generische Algorithmen ein (z. B. C# mit LINQ, Java mit Streams). In Go sind dafür entweder generische Typen oder – wie im unten angeführten Beispiel zu sehen – generische Methoden zu verwenden. Eine standardisierte, generische Iterator-Implementierung, die sich auch in die Sprache integrieren ließe (bspw. in Verbindung mit range), wäre denkbar, es gibt dafür aber derzeit keine konkreten Pläne.
Das folgende Codebeispiel zeigt, wie man eine generische Iterator-Funktion mit Go Generics implementieren könnte. Auf Basis der Iterator-Funktion werden dann exemplarisch die Funktionen iteratorFromSlice, forEach und filter implementiert. Am Ende des Beispiels sieht man die Funktionen auf ein Slice von Objekten (user) sowie auf eine zur Laufzeit generierte Liste von Zahlen (numbersIterator) angewandt:
// Generic iterator function type
type iteratorFunc[T any] func() *T;
// Generic function for iteration
func next[T any](iterator iteratorFunc[T]) *T { return iterator() }
// Generic function executing a given function for each item in iterator
func forEach[T any](iterator iteratorFunc[T], body func(T)) {
for ptr := next(iterator); ptr != nil; ptr = next(iterator) {
body(*ptr)
}
}
// Generic predicate
type predicate[T any] func(item T) bool;
// Generic function filtering based on a given predicate
func filter[T any](iterator iteratorFunc[T], predicate func(T) bool) iteratorFunc[T] {
return func() *T {
var item *T
for item = next(iterator); item != nil && !predicate(*item); item = next(iterator) { }
return item
}
}
// Generic function that generates an iterator from a given slice
func iteratorFromSlice[T any](items []T) iteratorFunc[T] {
return func() *T {
if len(items) < 1 {
return nil
}
firstItem := &items[0]
items = items[1:]
return firstItem
}
}
type user struct {
name string
age int
}
func numbersIterator(max int) iteratorFunc[int] {
current := 0;
return func() *int {
if (current >= max) {
return nil
}
result := current
current++
return &result
}
}
func main() {
users := []user{
user{name: "Foo", age: 42},
user{name: "Bar", age: 43},
user{name: "FooBar", age: 44},
}
// Print each user's name where the user name starts with Foo.
forEach(
filter(
iteratorFromSlice(users),
func(u user) bool { return strings.HasPrefix(u.name, "Foo"); }),
func(u user) { fmt.Printf("User is %s\n", u.name) })
// Print even numbers between 0 and 10 (excl.)
forEach(
filter(
numbersIterator(10),
func(n int) bool { return n % 2 == 0 }),
func(n int) { fmt.Println(n) })
}