Einleitung: Was sind Generics und warum wurden sie in Go eingeführt
Lange Zeit kam die Go-Community ohne Generics (generische Typen) aus. Entwickler verwendeten interface{} und manuelle Typumwandlungen, was zu umständlichem Code und Laufzeitfehlern führte. Mit der Veröffentlichung von Go 1.18 (Februar 2022) wurden Generics offiziell in die Sprache aufgenommen – eine der am meisten erwarteten Funktionen.
Generics (oder parametrischer Polymorphismus) ermöglichen es, Funktionen und Datenstrukturen zu schreiben, die mit beliebigen Typen arbeiten, während sie dennoch eine strenge Typisierung zur Kompilierzeit beibehalten. Das bedeutet, dass Sie einen Algorithmus (z. B. Sortierung oder Suche) schreiben und ihn für int, string, benutzerdefinierte Strukturen verwenden können – ohne Code-Duplizierung und ohne Verlust der Typsicherheit.
In diesem Artikel werden wir untersuchen, warum Generics in Go benötigt werden, wie man sie richtig verwendet und welche Fallstricke zu beachten sind.
Hauptteil: Wie Generics in Go funktionieren
1. Syntax der Typparameter
Die grundlegende Syntax von Generics in Go sieht wie folgt aus: Nach dem Funktions- oder Typnamen werden in eckigen Klammern die Typparameter angegeben. Betrachten wir ein einfaches Beispiel – eine Funktion, die den übergebenen Wert zurückgibt (Analogon zu identity):
package main
import "fmt"
// Generische Funktion: T ist ein Typparameterfunc Print[T any](value T) { fmt.Println(value)}
func main() { Print[int](42) // explizite Typangabe Print[string]("Go") // explizite Typangabe Print("Go 1.18") // implizite Typinferenz (type inference)}Das Schlüsselwort any ist ein Alias für interface{}. Es bedeutet, dass die Funktion einen Wert beliebigen Typs akzeptiert. Die wahre Stärke von Generics zeigt sich jedoch bei der Verwendung von Constraints (Einschränkungen).
2. Constraints (Einschränkungen) und Interfaces
Constraints ermöglichen es, anzugeben, welche Operationen mit dem Typparameter ausgeführt werden können. Um beispielsweise zwei Zahlen zu addieren, muss sichergestellt sein, dass der Typ die Operatoren +, -, * usw. unterstützt. In Go werden dafür spezielle Interfaces aus dem Paket golang.org/x/exp/constraints oder benutzerdefinierte Interfaces verwendet:
package main
import ( "fmt" "golang.org/x/exp/constraints")
// Einschränkung: T muss Vergleich unterstützenfunc Max[T constraints.Ordered](a, b T) T { if a > b { return a } return b}
func main() { fmt.Println(Max(10, 20)) // 20 fmt.Println(Max("apple", "banana")) // banana // fmt.Println(Max([]int{1}, []int{2})) // Kompilierfehler: Slices sind nicht geordnet}Das eingebaute Interface constraints.Ordered umfasst Typen, die die Operatoren <, <=, >, >= unterstützen: alle numerischen Typen und string.
Sie können auch eigene Constraints erstellen:
// Benutzerdefinierte Einschränkung: Der Typ muss eine Methode String() habentype Stringer interface { String() string}
func Describe[T Stringer](v T) string { return "Value: " + v.String()}3. Generische Strukturen und Methoden
Generics in Go funktionieren nicht nur mit Funktionen, sondern auch mit benutzerdefinierten Typen. Betrachten wir ein klassisches Beispiel – einen generischen Stack:
package main
import "fmt"
// Generische Struktur Stacktype Stack[T any] struct { items []T}
// Push fügt ein Element zum Stack hinzufunc (s *Stack[T]) Push(item T) { s.items = append(s.items, item)}
// Pop entfernt ein Element aus dem Stackfunc (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}
func main()