Menu

Golang Generics: Typparameter, Constraints und Beispiele

Mit Generics (ab Go 1.18) funktioniert eine Funktion oder ein Typ mit vielen Typen und bleibt dabei typsicher. Typparameter, die Constraints any, comparable und cmp.Ordered, eigene Constraints mit ~, generische Typen und wann du sie besser nicht nutzt.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Eine generische Funktion

Typparameter stehen in eckigen Klammern vor den normalen Parametern. Jeder hat ein Constraint, das festlegt, welche Typen erlaubt sind.

T und U werden aus den Argumenten abgeleitet, also schreibst du selten Map[int, string](...). Die Inferenz arbeitet mit den Funktionsargumenten; einen Typ, der nur im Ergebnis vorkommt, kann sie nicht ableiten. Bei func Zero[T any]() T musst du Zero[int]() schreiben.

Vor Go 1.18 musste diese Funktion für jeden Typ einzeln geschrieben werden oder []interface{} nehmen und die Typsicherheit verlieren.

Constraints

Ein Constraint ist ein Interface. Es begrenzt, welche Typen verwendet werden können, und sagt dem Compiler im Gegenzug, welche Operationen auf Werten dieses Typs erlaubt sind.

ConstraintErlaubtDu kannst nutzen
anyjeden TypZuweisung, Übergabe, Speichern
comparableTypen mit ====, !=, Map-Schlüssel
cmp.Ordered (Go 1.21)Ganzzahlen, Floats, Strings<, <=, >, >=, dazu ==
ein Interface mit MethodenTypen mit diesen MethodenAufruf der Methoden
eine Typ-Union wie ~int | ~float64genau diese TypenOperatoren, die alle davon unterstützen

Mit any statt comparable kompiliert v == target nicht: invalid operation: v == target (incomparable types in type set). Erst das Constraint macht den Operator erlaubt.

Diese beiden Helfer gibt es schon: slices.Index und slices.Max. Die Standardpakete slices, maps und cmp decken den meisten generischen Alltagsbedarf ab, also schau dort nach, bevor du deine eigenen schreibst.

Eigene Constraints mit Typ-Unions und ~

Ein Constraint kann Typen mit | auflisten. Erlaubt sind die Operatoren, die jeder aufgelistete Typ unterstützt.

Die Tilde zählt. ~int64 bedeutet „jeder Typ, dessen zugrunde liegender Typ int64 ist“, also wird der benannte Typ Cents akzeptiert. Mit einem schlichten int64 scheitert Sum([]Cents{...}) mit Cents does not satisfy Number (possibly missing ~ for int64 in Number).

Interfaces mit Typ-Unions können nur als Constraints verwendet werden, nie als gewöhnliche Variablentypen. var n Number kompiliert nicht.

Das Paket golang.org/x/exp/constraints bietet fertige Constraints wie Integer, Float und Signed. Es liegt außerhalb der Standardbibliothek, also kann der Runner hier es nicht importieren; cmp.Ordered ist das Standard-Constraint.

Generische Typen

Auch Structs, Slices, Maps und andere Typen können Typparameter haben. Methoden auf einem generischen Typ verwenden die Parameter des Receivers.

Zwei Details aus diesem Beispiel tauchen ständig auf:

  • Der Nullwert von T. Mit var zero T gibst du in generischem Code „nichts“ zurück. Ein T{} oder nil, das für jeden Typ funktioniert, gibt es nicht.
  • Instanziierung. Ein generischer Typ muss vor der Verwendung instanziiert werden: Stack[string], nicht Stack. In seinen eigenen Methoden schreibst du den Receiver als Stack[T].

Go 1.24 hat generische Typ-Aliase eingeführt: type Set[T comparable] = map[T]struct{} kompiliert jetzt.

Grenzen, auf die du stoßen wirst

  • Keine Typparameter an Methoden. func (s *Stack[T]) Map[U any](...) ist nicht erlaubt. Nimm eine Funktion auf oberster Ebene.
  • Keine Spezialisierung. Du kannst keine eigene Implementierung für T = string schreiben. Ein Type Switch auf any(v) in der Funktion ist der Workaround und meist ein Hinweis, dass Generics das falsche Werkzeug sind.
  • Keine Operator-Constraints per Name. Du kannst nicht „jeder Typ mit einer +-Methode“ sagen; du listest die Typen in einer Union auf.
  • Feldzugriff über ein Constraint funktioniert nicht. Selbst wenn jeder Typ in einer Union ein Feld ID hat, ist v.ID nicht erlaubt. Nimm stattdessen eine Methode ins Constraint.

Wann du keine Generics nutzen solltest

Generics passen zu Container-Typen (Stacks, Sets, Caches, Bäume) und zu Algorithmen über Slices und Maps, die für jeden Elementtyp dasselbe tun. Sie passen nicht überall.

  • Ein einziger konkreter Typ. Wird die Funktion immer nur mit []User aufgerufen, kostet eine generische Version Lesezeit und bringt sonst nichts.
  • Verhalten, das sich je nach Typ unterscheidet. Dafür gibt es Interfaces. Eine Funktion, die einen io.Writer nimmt, ist einfacher als eine mit [T io.Writer] und funktioniert genauso.
  • Interface-Parameter ersetzen. func Print[T fmt.Stringer](v T) ist nicht besser als func Print(v fmt.Stringer).

Die Richtlinie des Go-Teams selbst: Schreib den Code zuerst für einen bestimmten Typ und greif zu Typparametern, wenn du merkst, dass du denselben Code ein zweites Mal schreibst und sich nur die Typen ändern.

Häufige Fehler

  • any verwenden, wo comparable oder cmp.Ordered nötig ist. Der Compiler lehnt == oder < auf einem Typparameter mit any ab.
  • Das ~ vergessen. Selbst definierte Typen wie type ID int erfüllen ein schlichtes int-Constraint nicht.
  • nil für einen Typparameter zurückgeben. Nicht erlaubt, außer das Constraint beschränkt T auf pointerartige Typen. Gib einen Nullwert zurück.
  • Einen generischen Helfer schreiben, den es schon gibt. Schau zuerst in slices, maps und cmp.

Häufig gestellte Fragen

Hat Go Generics?

Ja, seit Go 1.18 (März 2022). Funktionen und Typen können Typparameter in eckigen Klammern deklarieren: func Map[T, U any](s []T, f func(T) U) []U. Jeder Typparameter hat ein Constraint, ein Interface, das beschreibt, was der Typ unterstützen muss.

Was ist der Unterschied zwischen any und comparable bei Go-Generics?

any erlaubt jeden Typ, aber mit den Werten kannst du fast nichts tun außer sie zuzuweisen, zu übergeben und zu speichern. comparable erlaubt nur Typen, die == und != unterstützen, und das brauchst du für Map-Schlüssel oder Gleichheitsprüfungen. Für < und > nimm cmp.Ordered.

Was bedeutet die Tilde (~) in einem Go-Constraint?

~T bedeutet „jeder Typ, dessen zugrunde liegender Typ T ist“. ~int passt auf int und auch auf type Celsius int. Ohne Tilde passt int in einem Constraint nur auf int selbst, also würden selbst definierte Typen abgelehnt.

Können Go-Methoden Typparameter haben?

Nein. Eine Methode kann die Typparameter ihres Receiver-Typs verwenden (func (s *Stack[T]) Push(v T)), aber keine neuen deklarieren. Schreib stattdessen eine generische Funktion auf oberster Ebene: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S