Mit Go 1.18 im März 2022 hat die Sprache nach über einem Jahrzehnt Entwicklungszeit Generics bekommen — die Möglichkeit, Funktionen und Typen so zu schreiben, dass sie über mehrere konkrete Typen funktionieren, ohne dass du die Typsicherheit verlierst. Der Mechanismus heißt Type Parameters: eine Liste von Typ-Platzhaltern in eckigen Klammern, die bei der Verwendung durch konkrete Typen ersetzt werden. Dieser Artikel ist die Übersicht zum Generics-Teil der Doku — er erklärt die Syntax aus der Sprach-Spec, wie Instantiation und Type Inference zusammenspielen, was du mit generischen Typen und Methoden tun darfst (und was nicht), und wie Gos Generics-Modell sich bewusst von C++-Templates und Java-Generics unterscheidet.
Wozu Generics überhaupt — und warum erst seit Go 1.18
Vor Go 1.18 hatte die Sprache zwei Werkzeuge, um „Code über mehrere Typen" zu schreiben: interface{} mit Type-Assertion und Code-Generierung über go generate. Beide kosten etwas. Das leere Interface ist typunsicher zur Compile-Zeit — du verlierst die statische Garantie, dass dein Aufrufer den richtigen Typ übergibt, und musst bei jedem Zugriff casten. Code-Generierung umgeht das, bläht aber das Build-System auf und verteilt nahezu identischen Code über mehrere Dateien.
Die typische Vor-Generics-Lösung sah so aus: man schrieb eine Funktion, die any (bzw. interface{} vor Go 1.18) akzeptiert, und der Aufrufer musste jedes Ergebnis zurückcasten. Das war fehleranfällig und brachte Laufzeit-Overhead:
package main
import "fmt"
// Vor Go 1.18: alles als interface{} (any) durchreichen
func minAny(a, b any) any {
ai, _ := a.(int)
bi, _ := b.(int)
if ai < bi {
return ai
}
return bi
}
func main() {
// Aufrufer muss casten und auf Korrektheit hoffen
r := minAny(3, 5).(int)
fmt.Println(r)
}3Drei Probleme stechen sofort hervor. Erstens: minAny funktioniert ausschließlich für int, obwohl die Signatur „irgendetwas" verspricht — eine Lüge des Typsystems. Zweitens: jeder Type-Assert kann zur Laufzeit fehlschlagen, und nichts im Compiler warnt dich, falls du an einer Stelle string-Werte durchreichst. Drittens: der Aufrufer muss das Ergebnis manuell zurückcasten (.(int)), bevor er es weiterverwenden kann. Mit Generics verschwindet das: der Compiler kennt den Typ, prüft ihn statisch, und es gibt keinen Cast.
Der Go-Blog-Post zur Generics-Einführung formuliert das Designziel knapp: „Generics are a way of writing code that is independent of the specific types being used." Die Sprache bekommt also einen Weg, typunabhängigen Code mit statischer Typprüfung zu schreiben — genau die Mitte zwischen den beiden bisherigen Extremen.
Syntax der Type-Parameter-Deklaration
Die Go-Spec definiert Type-Parameter-Listen über eine eigene Grammatik. Sie sind syntaktisch eine zweite Parameter-Liste, die direkt nach dem Funktions- oder Typ-Namen steht und durch eckige Klammern statt runde abgegrenzt wird:
TypeParameters = "[" TypeParamList [ "," ] "]" .
TypeParamList = TypeParamDecl { "," TypeParamDecl } .
TypeParamDecl = IdentifierList TypeConstraint .Lies das langsam: jeder TypeParamDecl besteht aus einer Identifier-Liste und einer TypeConstraint. Die Identifier sind die Namen, unter denen du den Platzhalter im Funktions-Körper benutzt — Konvention ist ein einzelner Großbuchstabe wie T, K, V, E. Die Constraint ist ein Typ-Element (in den allermeisten Fällen ein Interface), das den Type Set definiert: die Menge der erlaubten Typen, die dieser Parameter annehmen darf. Die Spec erklärt das so:
A type parameter list declares the type parameters of a generic function or type declaration. The type parameter list looks like an ordinary function parameter list except that the type parameter names must all be present and the list is enclosed in square brackets rather than parentheses.
Was die Spec damit meint: die Form [T any] ist strukturell genau wie (x int) — Name plus Typ-Information. Nur dass der „Typ" hier eine Constraint ist, also eine Menge erlaubter Typen, und das Ganze in eckigen Klammern steht. Die Wahl der eckigen Klammern statt der spitzen <...> wie in Java oder C++ war bewusst: spitze Klammern erzeugen Mehrdeutigkeiten mit Vergleichs-Operatoren und sind in einem LL(1)-tauglichen Parser teuer.
Damit du ein Gefühl für die Vielfalt der zulässigen Formen bekommst — hier vier Beispiele aus der Spec, alle syntaktisch gültig:
// Ein Parameter, beliebiger Typ
func F1[P any]() {}
// Constraint per Type-Set: nur byte-Slices oder Strings
func F2[S interface{ ~[]byte | string }](s S) {}
// Zwei Parameter; E wird im Constraint von S verwendet
func F3[S ~[]E, E any](s S) {}
// Constraint, die selbst eine generische Interface ist
func F4[P Constraint[int]]() {}Beobachtungen, die wichtig sind: Erstens darfst du in der Constraint eines Parameters einen anderen Parameter referenzieren — bei F3 wird E benutzt, um S als Slice-Typ zu beschreiben (~[]E). Zweitens kann die Constraint eine eigene generische Interface sein (Constraint[int] in F4), die bereits instanziiert wurde. Drittens darfst du einen Type-Parameter _ nennen, falls du ihn im Code nicht referenzieren musst — das ist selten sinnvoll, aber syntaktisch erlaubt.
Die erste konkrete Funktion — Min Token für Token
Genug Grammatik. Schauen wir uns die kanonische Generics-Funktion an: ein typunabhängiges Min. Die Standard-Bibliothek liefert seit Go 1.21 das Paket cmp mit dem Interface cmp.Ordered, das alle „natürlich vergleichbaren" Typen umfasst — int, float64, string und ihre Verwandten. Damit lässt sich Min so schreiben:
package main
import (
"cmp"
"fmt"
)
// Min gibt das Minimum zweier Werte zurück.
// T ist ein Type-Parameter mit Constraint cmp.Ordered:
// erlaubt sind alle Typen, auf denen < definiert ist.
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(3, 7)) // T = int
fmt.Println(Min(2.5, 1.25)) // T = float64
fmt.Println(Min("go", "c++")) // T = string
}3
1.25
c++Gehen wir jedes Token in der Deklaration func Min[T cmp.Ordered](a, b T) T einzeln durch. func ist das gewohnte Keyword. Min ist der Funktions-Name. Die eckigen Klammern [T cmp.Ordered] öffnen die Type-Parameter-Liste — innerhalb davon deklariert T cmp.Ordered einen einzigen Type-Parameter namens T mit der Constraint cmp.Ordered. Die runden Klammern (a, b T) sind die gewohnte Werte-Parameter-Liste — a und b haben beide den Typ T, also denselben (noch unbekannten) Typ. Das abschließende T ist der Rückgabe-Typ, ebenfalls der Type-Parameter.
Im Funktions-Körper verhält sich T wie ein normaler Typ: a < b ist erlaubt, weil cmp.Ordered garantiert, dass der <-Operator auf T definiert ist. Wäre die Constraint stattdessen any, würde der Compiler den Vergleich verweigern — any lässt jeden Typ zu, also auch solche, für die < undefiniert ist (struct-Typen ohne Vergleichs-Methode beispielsweise). Die Constraint kontrolliert nicht nur, welche Typen du übergeben darfst, sondern auch welche Operationen du im Körper benutzen darfst — das ist der zentrale Kontrakt.
Instantiation — vom generischen zum konkreten
Eine generische Funktion ist kein aufrufbarer Wert. Die Spec ist hier präzise:
A generic function must be instantiated before it can be called or used as a value.
Instantiation heißt: die Type-Parameter mit konkreten Typ-Argumenten füllen. Das passiert in zwei Schritten, die der Compiler dir abnimmt. Der Intro-Blog-Post beschreibt sie so: „First, the compiler substitutes all type arguments for their respective type parameters throughout the generic function or type. Second, the compiler verifies that each type argument satisfies the respective constraint." Du gibst also einen Typ rein, der Compiler ersetzt jedes Vorkommen von T durch diesen Typ, und prüft dann, dass das Ergebnis im Type Set der Constraint liegt.
Es gibt zwei Schreibweisen für die Instantiation. Die explizite Form führt die Type-Argumente in eckigen Klammern direkt nach dem Funktions-Namen auf:
package main
import (
"cmp"
"fmt"
)
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
// Explizit: Min wird mit T=int instanziiert,
// dann mit (2, 3) aufgerufen.
r := Min[int](2, 3)
fmt.Println(r)
// Du kannst die instanziierte Funktion auch als Wert binden:
minInt := Min[int]
fmt.Println(minInt(10, 20))
}2
10Min[int] ist ein Ausdruck mit dem konkreten Typ func(int, int) int — eine ganz normale Funktion, die du aufrufen, in einer Variable speichern oder weitergeben kannst. Der generische Name Min allein ist kein gültiger Funktions-Wert; erst die Instantiation macht ihn dazu.
Der häufigere Weg ist die Type Inference: du nennst nur die Werte-Argumente, der Compiler schließt von ihren Typen auf die Type-Argumente:
package main
import (
"cmp"
"fmt"
)
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
// Compiler schließt T=int aus den Argumenten.
fmt.Println(Min(2, 3))
// T=float64
fmt.Println(Min(2.5, 3.5))
// T=string
fmt.Println(Min("alice", "bob"))
// Ohne Inferenz brauchst du Hilfe: kein Werte-Argument
// mit Typ-Information zur Verfügung
// → Compile-Fehler:
// var m = Min // Min ohne Type-Argument ist kein Wert
_ = Min[int] // OK
}2
2.5
aliceDie Mechanik der Inferenz: Go schaut sich für jede Position in der Argument-Liste den deklarierten Parameter-Typ an. Bei Min(2, 3) ist Position 1 als T deklariert und das Argument 2 ein int — also T = int. Position 2 ist ebenfalls T, das Argument 3 ist ebenfalls int — konsistent. Damit ist die Inferenz erfolgreich, und die Funktion wird als Min[int] instanziiert. Hätte der Aufrufer Min(2, 3.5) geschrieben, wäre Position 1 int und Position 2 float64 — inkonsistent, der Compiler verweigert die Inferenz und fordert eine explizite Type-Argument-Angabe oder eine Konvertierung.
Grenzen der Type Inference
Inferenz funktioniert, solange jeder Type-Parameter mindestens einmal in einer Position auftaucht, deren Wert beim Aufruf bekannt ist. Die naheliegende Position ist die Werte-Parameter-Liste. Wenn ein Type-Parameter aber nur im Rückgabe-Typ oder im Funktions-Körper steht, kann der Compiler ihn aus dem Aufruf allein nicht erschließen — du musst explizit instanziieren.
package main
// T taucht nur im Rückgabetyp auf — Inferenz kann
// den Typ aus dem Aufruf nicht ermitteln.
func MakeZero[T any]() T {
var zero T
return zero
}
func main() {
// FEHLER: kann T nicht inferieren
// _ = MakeZero()
// OK: explizit
_ = MakeZero[int]()
_ = MakeZero[string]()
}Das ist kein Bug, sondern Designentscheidung. Go versucht bewusst nicht, aus dem Kontext der zuweisenden Variable (var x int = MakeZero()) zu inferieren — solche kontext-basierte Inferenz macht Fehlermeldungen mehrdeutig und Tooling schwerer. Wenn du den Zwang zur expliziten Instantiation vermeiden willst, schreibe die API anders: füge einen unbenutzten Parameter vom richtigen Typ hinzu oder zwinge den Aufrufer, einen Wert zur Verfügung zu stellen, aus dem inferiert werden kann.
Inferenz hat noch eine zweite Quelle, die Constraint Type Inference: wenn eine Constraint genug Struktur hat, kann der Compiler aus einem bereits bekannten Parameter andere ableiten. Bei func F[S ~[]E, E any](s S) reicht es, das Werte-Argument zu betrachten — daraus folgt S = []int zum Beispiel, und über die Constraint ~[]E zwangsläufig E = int. Du kannst also F([]int{1, 2, 3}) schreiben, ohne E explizit zu nennen, obwohl E nur in der Constraint von S vorkommt.
Mehrere Type-Parameter — Map als Lehrbuch-Fall
Eine Funktion darf beliebig viele Type-Parameter haben. Ein Klassiker funktionaler Programmierung ist Map: nimm eine Slice und eine Funktion, gib eine Slice mit den transformierten Elementen zurück. In Go braucht das zwei unabhängige Type-Parameter — den Eingabe-Element-Typ und den Ausgabe-Element-Typ.
package main
import "fmt"
// Map transformiert jede Element-Position einer Slice
// über f und liefert die Ergebnis-Slice zurück.
// A ist der Quelltyp, B der Zieltyp.
func Map[A, B any](xs []A, f func(A) B) []B {
out := make([]B, len(xs))
for i, x := range xs {
out[i] = f(x)
}
return out
}
func main() {
nums := []int{1, 2, 3, 4}
// A=int, B=int — Verdopplung
doubled := Map(nums, func(n int) int { return n * 2 })
fmt.Println(doubled)
// A=int, B=string — Typ-Wandel
labels := Map(nums, func(n int) string {
return fmt.Sprintf("#%d", n)
})
fmt.Println(labels)
}[2 4 6 8]
[#1 #2 #3 #4]Map[A, B any] deklariert zwei Parameter, beide mit Constraint any. Die Kurz-Schreibweise A, B any setzt für beide Identifier dieselbe Constraint — das spart Tipparbeit, wenn du mehrere Parameter mit derselben Constraint hast. Inferenz funktioniert, weil A über das Argument xs (ein []A) und über den Funktions-Parameter von f (ein func(A)) abgeleitet werden kann, und B über den Rückgabetyp von f. Beide Parameter haben also „Anker" in der Aufruf-Signatur.
Beachte: ohne Generics müsstest du Map separat für jedes A/B-Paar schreiben — MapIntToInt, MapIntToString, MapStringToBool und so weiter. Oder du würdest any mit Cast-Sturm nehmen. Beides ist hässlich; Generics machen es genau einmal.
Generische Typen — nicht nur Funktionen
Type-Parameter sind nicht auf Funktionen beschränkt. Auch Typ-Definitionen dürfen welche tragen — das ist der Mechanismus für generische Container-Typen wie Listen, Bäume, Stacks oder Caches. Die Spec sagt dazu kurz: „If the type definition specifies type parameters, the type name denotes a generic type. Generic types must be instantiated when they are used."
Hier ein einfacher Stack:
package main
import "fmt"
// Stack ist generisch über T — die Elemente sind alle vom Typ T.
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(x T) {
s.items = append(s.items, x)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
n := len(s.items) - 1
x := s.items[n]
s.items = s.items[:n]
return x, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
// Instantiation des Typs: Stack[int] ist ein konkreter Typ.
var s Stack[int]
s.Push(1)
s.Push(2)
s.Push(3)
for s.Len() > 0 {
v, _ := s.Pop()
fmt.Println(v)
}
}3
2
1Drei Dinge an dieser Deklaration sind beachtenswert. Erstens: Stack[T any] ist kein Typ — es ist eine Typ-Vorlage. Erst Stack[int] ist ein konkreter, verwendbarer Typ. Du kannst keine Variable als var s Stack deklarieren; das gibt einen Compile-Fehler. Zweitens: die Methoden-Receiver tragen die Type-Parameter mit. func (s *Stack[T]) Push(x T) — der Receiver-Typ ist *Stack[T], mit demselben T, das in der Typ-Definition deklariert wurde. Drittens: innerhalb der Methode ist T wie ein gewöhnlicher Typ verwendbar. var zero T deklariert eine Variable mit dem Zero Value des (zur Aufruf-Zeit konkreten) Typs — das wird gleich noch wichtig.
Methoden — wichtige Einschränkung
Methoden eines generischen Typs sind „mit-generisch" — sie erben die Type-Parameter des Receivers. Was Go nicht erlaubt, ist eine Methode mit eigenen, zusätzlichen Type-Parametern. Diese Restriktion ist eine bewusste Sprachentscheidung; das Go-Team hat sie 2022 ausführlich diskutiert und vorerst ausgeschlossen, um die Interaktion mit Interfaces einfach zu halten.
package main
type Box[T any] struct {
value T
}
// OK: Methode benutzt den Receiver-Type-Parameter T.
func (b Box[T]) Value() T {
return b.value
}
// FEHLER: Methode versucht, einen eigenen Type-Parameter U
// einzuführen — Go verbietet das.
//
// func (b Box[T]) MapTo[U any](f func(T) U) Box[U] {
// return Box[U]{value: f(b.value)}
// }
//
// compile error: method must have no type parametersDer idiomatische Ausweg: schreib die transformierende Operation als freie Funktion statt als Methode. Statt box.MapTo[U](f) schreibst du MapBox[T, U](box, f). Das fühlt sich aus der OO-Welt fremd an, ist aber die einzige Form, die Go heute kennt. Der Hintergrund: würde Go Methoden mit eigenen Type-Parametern erlauben, müsste auch das Interface-Modell darauf reagieren — und die Frage, was es heißt, dass ein Wert das Interface interface{ M[T any]() } „implementiert", ist nicht eindeutig zu beantworten, ohne in Java-artige Reification-Probleme zu laufen.
Zero-Value-Problem bei Type-Parametern
Im Pop-Beispiel oben steht die Zeile var zero T. Das funktioniert, aber sie verdient eine eigene Betrachtung, weil sie eine konzeptuelle Falle aufzeigt. Innerhalb einer generischen Funktion oder Methode weißt du den konkreten Typ von T nicht — er ist zur Schreib-Zeit unbekannt. Du kannst trotzdem eine Variable vom Typ T deklarieren; sie bekommt den Zero Value des (später konkreten) Typs.
package main
import "fmt"
func ZeroOf[T any]() T {
var zero T
return zero
}
func main() {
fmt.Printf("%#v\n", ZeroOf[int]()) // 0
fmt.Printf("%#v\n", ZeroOf[string]()) // ""
fmt.Printf("%#v\n", ZeroOf[[]int]()) // nil
fmt.Printf("%#v\n", ZeroOf[*int]()) // (*int)(nil)
}0
""
[]int(nil)
(*int)(nil)Das ist nützlich: du kannst eine generische Funktion schreiben, die „etwas Leeres" zurückgibt, ohne den konkreten Typ zu kennen. Aber Achtung — du kannst nicht ohne Weiteres nil als Wert für ein beliebiges T zuweisen. return nil funktioniert nur, wenn die Constraint von T garantiert, dass T ein Referenz-Typ ist (Pointer, Slice, Map, Channel, Func, Interface). Bei T any lässt das die Sprache nicht zu, weil int kein nil kennt. Der idiomatische Weg ist daher immer var zero T; return zero.
Vergleichbar gibt es kein generisches „erzeuge mir eine neue Instanz von T" mit Konstruktor-Logik. new(T) funktioniert und gibt *T zurück — das ist hilfreich, wenn du in einer generischen Datenstruktur einen Pointer auf ein frisches Element brauchst. Aber T{} als Composite Literal ohne weiteres Wissen über T ist nicht erlaubt — du müsstest die Struktur über die Constraint erzwingen, was nur für Structs sinnvoll wäre.
Type Sets und Method Sets — kurzer Vorgriff
Constraints sind in Go Interfaces — aber Interfaces in einer erweiterten Rolle. Bis Go 1.17 beschrieb ein Interface ausschließlich einen Method Set: die Menge der Methoden, die ein Typ haben muss, um das Interface zu erfüllen. Seit 1.18 darf ein Interface zusätzlich einen Type Set beschreiben — eine direkte Liste erlaubter Typen, durch Union-Operator | und Approximations-Operator ~ ausgedrückt.
// Method Set — klassisch, Methoden-basiert
type Stringer interface {
String() string
}
// Type Set — neu seit Go 1.18, Typ-basiert
type Number interface {
int | int32 | int64 | float32 | float64
}
// Mit ~ : alle Typen, deren Underlying-Type int ist
// (z. B. eigene Aliase: type Age int → erlaubt)
type AnyInt interface {
~int
}
// Kombiniert: Type Set UND Method Set
type PrintableNumber interface {
~int | ~float64
String() string
}Der separate Artikel zu Constraints behandelt das im Detail. Für die Übersicht reicht: eine Constraint ist immer ein Interface, und Interfaces können seit 1.18 mehr als nur Methoden enthalten. Wenn die Constraint einen Method Set hat, kannst du im Funktions-Körper die Methoden aufrufen. Wenn sie einen Type Set hat, kannst du die Operatoren benutzen, die auf allen Typen des Sets definiert sind — bei int | float64 zum Beispiel +, -, *, /, <, ==.
Mechanik unter der Haube — GC Shape Stenciling
C++-Templates erzeugen für jeden konkreten Typ eine eigene Kopie des Codes (monomorphisierung). Java-Generics dagegen löschen die Type-Parameter zur Laufzeit komplett (type erasure) und arbeiten intern mit Object. Beide Ansätze haben bekannte Probleme — Binärgrößen-Explosion bei C++, Boxing-Kosten und Reflection-Grenzen bei Java.
Go geht einen dritten Weg, der intern GC Shape Stenciling heißt. Vereinfacht: der Compiler erzeugt eine kompilierte Kopie pro „GC shape" — pro Speicher-Layout, das aus Sicht des Garbage Collectors gleich aussieht. Alle Pointer-Typen teilen sich eine Kopie. Alle int-Größen-gleichen Werte (int, int64, uintptr) teilen sich eine Kopie. Strings teilen sich eine. Channels, Maps, Interfaces — jeweils eine Kopie.
Das hat Konsequenzen, die du als API-Designer im Hinterkopf haben solltest. Erstens: Generics in Go sind nicht per-Typ-monomorphisiert wie C++, daher gibt es keine spektakuläre Binärgrößen-Explosion. Zweitens: aber die Funktionen brauchen zur Laufzeit ein Dictionary — ein verstecktes Argument, das den konkreten Typ beschreibt (Größe, Pointer-Layout, gegebenenfalls Methoden). Das kostet einen kleinen Overhead pro Aufruf. Drittens: in seltenen Pfaden (Inlining nicht möglich, Pointer-Indirektion durchs Dictionary) kann generischer Code messbar langsamer sein als handgeschriebener typspezifischer Code. Für die allermeisten Anwendungen ist das irrelevant; in heißen Loops mit kleinen Operationen kann es sich rechnen, eine spezialisierte Variante zu pflegen.
Was Go bewusst NICHT hat
Wer aus C++ oder Java kommt, wird ein paar Features vermissen — die Abwesenheit ist Absicht, nicht Auslassung. Eine kurze Abgrenzung:
| Feature | C++ | Java | Go 1.22+ |
|---|---|---|---|
| Generische Funktionen / Typen | ja | ja | ja |
| Type Inference an der Call-Site | begrenzt | ja | ja |
Template-Spezialisierung (eigener Code für int) | ja | nein | nein |
| Operator-Overloading durch Generics | ja | nein | nein |
| Generische Methoden mit eigenem Type-Parameter | ja | ja | nein |
| Variadic Type Parameters | ja (Variadic Templates) | nein | nein |
| Higher-Kinded Types | nein | nein | nein |
| Type Erasure zur Laufzeit | nein | ja | nein (GC Shape) |
Die wichtigsten praktischen Lücken: keine Spezialisierung (du kannst nicht „aber für int mach es anders") — wenn du das brauchst, schreibst du zwei separate Funktionen. Kein Operator-Overloading — du kannst keinen eigenen Typ definieren, der + für deinen Anwendungsfall überlädt, weil Operatoren in Go an die Sprache gebunden sind und nicht über Interfaces erweiterbar. Und keine Methoden mit eigenen Type-Parametern, wie oben besprochen.
Das Designteam um Ian Lance Taylor formuliert es als „write code first, not types" — Generics sind ein Werkzeug für bewiesene Muster der Duplikation, nicht der erste Hebel beim API-Design. Wer abstrakte Interfaces vermeidet, weil sie hier passend wären, und stattdessen Generics greift, baut tendenziell schwerer lesbaren Code.
Praxis 1 — generisches Max über alle ordered Typen
Genug Theorie, jetzt eine vollständige Datei aus der Praxis. Wir bauen Max analog zu Min, zeigen aber auch, dass es auf eigenen Typen funktioniert, deren Underlying-Type in cmp.Ordered ist. Das ist der Vorteil von ~ im Type Set:
package main
import (
"cmp"
"fmt"
)
// Max gibt das Maximum zweier Werte zurück.
// cmp.Ordered ist als Interface so definiert, dass
// alle nativ ordnenden Typen UND deren Aliase erlaubt sind.
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Eigener Typ über int — dank ~int in cmp.Ordered erlaubt.
type Score int
// Eigener Typ über string — ebenfalls erlaubt.
type Username string
func main() {
// Primitive Typen
fmt.Println(Max(3, 7)) // 7
fmt.Println(Max(2.5, 1.25)) // 2.5
fmt.Println(Max("apple", "pear"))// pear
// Eigene Typen — Inferenz erkennt Score und Username
s1, s2 := Score(120), Score(95)
fmt.Println(Max(s1, s2)) // 120
u1, u2 := Username("alice"), Username("bob")
fmt.Println(Max(u1, u2)) // bob
// Falsch: gemischte Typen unterbinden Inferenz
// fmt.Println(Max(Score(10), 5)) // compile error
// → explizit konvertieren:
fmt.Println(Max(Score(10), Score(5))) // 10
}7
2.5
pear
120
bob
10Beobachtung in der letzten auskommentierten Zeile: Max(Score(10), 5) schlägt fehl, obwohl 5 ein int ist und Score's Underlying-Type auch int ist. Der Grund ist die Inferenz-Regel: alle Werte-Argumente, die einem Type-Parameter zugeordnet sind, müssen exakt denselben Typ haben. Score und int sind aus Sicht des Typsystems verschieden — der explizite Cast Score(5) löst das. Das ist konsistent mit Go's strenger Typ-Disziplin: implizite Konvertierungen gibt es nicht, auch nicht in Generics.
Praxis 2 — Cache[K comparable, V any]
Ein zweites, realistisches Beispiel: ein einfacher typsicherer In-Memory-Cache mit zwei Type-Parametern. Schlüssel können beliebige vergleichbare Typen sein (Constraint comparable), Werte beliebig (Constraint any). Mit einem sync.RWMutex ist er nebenläufigkeitssicher.
package main
import (
"fmt"
"sync"
)
// Cache speichert Werte vom Typ V unter Schlüsseln vom Typ K.
// K muss comparable sein, weil Maps nur vergleichbare Keys akzeptieren.
type Cache[K comparable, V any] struct {
mu sync.RWMutex
store map[K]V
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
store: make(map[K]V),
}
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.store[key] = value
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.store[key]
return v, ok
}
func (c *Cache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.store, key)
}
func (c *Cache[K, V]) Len() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.store)
}
type User struct {
ID int64
Name string
}
func main() {
// Cache von int64 → User
users := NewCache[int64, User]()
users.Set(1, User{ID: 1, Name: "Alice"})
users.Set(2, User{ID: 2, Name: "Bob"})
if u, ok := users.Get(1); ok {
fmt.Printf("User 1: %+v\n", u)
}
fmt.Println("Anzahl Users:", users.Len())
// Zweiter Cache, völlig anderer Typ — derselbe Code.
tokens := NewCache[string, int]()
tokens.Set("alice", 42)
tokens.Set("bob", 17)
if n, ok := tokens.Get("alice"); ok {
fmt.Println("alice token:", n)
}
}User 1: {ID:1 Name:Alice}
Anzahl Users: 2
alice token: 42Was diese Implementierung zeigt: Generics machen einen Cache-Code zu beliebig vielen typsicheren Caches. Die Cache[int64, User]-Instanz nimmt nur int64-Keys und User-Werte an — der Compiler weist users.Get("foo") zur Compile-Zeit ab. Und der Code ist kürzer und klarer, nicht länger, als die interface{}-Variante mit ihrem Cast-Sturm. Das ist das Ziel.
Ein Detail zur Constraint-Wahl: K comparable ist nötig, weil map[K]V nur comparable-Keys erlaubt (das Go-Map-Built-in verlangt das schon immer). V any ist die liberalste Wahl — du legst keine Bedingung an die Werte. Du könntest die Constraint enger ziehen, falls du im Code Operationen auf V brauchst (z. B. V cmp.Ordered, falls du irgendwo sortieren willst). In diesem Cache reicht das Speichern und Zurückgeben, daher any.
Besonderheiten
Type-Parameter sind Compile-Zeit-Konstrukte.
Anders als bei Java-Generics gibt es kein „Type Erasure" zur Laufzeit, aber auch keine vollständige Monomorphisierung wie in C++. Go nutzt GC Shape Stenciling — der Compiler generiert eine Implementierung pro Speicher-Layout. Praktisch heißt das: Pointer-Generics teilen sich Code, primitive-Wert-Generics nicht.
any ist seit Go 1.18 ein Alias für interface{}.
Der Name any wurde mit den Generics eingeführt, weil interface{} als Constraint visuell unschön ist. Beide sind exakt äquivalent — du kannst sie beliebig vertauschen. In neuem Code ist any Konvention, sowohl als Constraint als auch im normalen Typsystem.
comparable ist eine eingebaute Constraint.
Sie umfasst alle Typen, auf denen == und != definiert sind: Zahlen, Strings, Bools, Pointer, Channels, Interfaces, Structs aus comparable-Feldern, Arrays davon. Slices, Maps und Funktionen sind nicht comparable. Wer Map-Keys oder Set-Elemente generisch will, braucht comparable.
Inferenz greift nicht über den Rückgabetyp.
Wenn ein Type-Parameter nur im Return-Typ vorkommt (func MakeT[T any]() T), kann der Compiler ihn aus dem Aufruf allein nicht ermitteln. Du musst explizit instanziieren: MakeT[int](). Das ist Designentscheidung, nicht Bug — kontext-basierte Inferenz macht Fehlermeldungen unverständlich.
Methoden dürfen keine eigenen Type-Parameter haben.
Eine Methode erbt die Type-Parameter ihres Receivers, kann aber keine zusätzlichen einführen. func (b Box[T]) Map[U any](...) ist nicht erlaubt. Der idiomatische Workaround: schreib es als freie Funktion MapBox[T, U any](b Box[T], f func(T) U) Box[U].
Constraint mit ~ erlaubt eigene Typen-Aliase.
~int bedeutet „alle Typen, deren Underlying-Type int ist" — also auch eigene Definitionen wie type Score int. Ohne ~ würde nur das exakte int zählen, und Aliase wären ausgeschlossen. cmp.Ordered nutzt durchgehend ~, deshalb funktioniert Min(Score(2), Score(3)).
Type-Parameter sind kein Ersatz für Interfaces.
Wenn deine Funktion nur Methoden eines Typs aufruft, nimm ein Interface — das ist einfacher und in Go idiomatischer. func ReadAll(r io.Reader) ist besser als func ReadAll[R io.Reader](r R). Generics zahlen sich erst aus, wenn du Werte vom Type-Parameter speicherst oder typsicher zurückgibst.
Generische Typ-Instantiation gilt auch für Methoden-Sets.
Stack[int] und Stack[string] sind aus Sicht des Typsystems unterschiedliche Typen, jeweils mit eigenem Method-Set. Du kannst keine Variable als Stack ohne Type-Argument deklarieren, und du kannst nicht implizit zwischen Stack[int] und Stack[any] konvertieren — auch wenn das logisch erscheint.
Weiterführende Ressourcen
Externe Quellen
- Type Parameter Declarations — Go Language Specification
- Instantiations — Go Language Specification
- An Introduction to Generics — The Go Blog
- When To Use Generics — The Go Blog
- Tutorial: Getting started with generics — go.dev
cmp.Ordered— Standard Library
Verwandte Artikel
- Constraints — Type Sets,
comparable, eigene Constraints - Wann Generics, wann Interface — Entscheidungs-Leitfaden
- Praktische Generics-Beispiele — Slice-Helfer, Maps, Container
- Empty Interface —
anyund Type-Assertions - Interfaces — Übersicht und Method-Sets
- Slice — Header, Backing-Array, Reslicing
- Map — Hash, Keys, Iteration
- Funktionen mit mehreren Rückgabewerten