Ein Constraint ist die Antwort auf eine einfache Frage: welche Typen darf der Aufrufer für diesen Type-Parameter einsetzen? In Go ist die Antwort syntaktisch immer ein Interface — aber ein Interface, das nicht nur ein Method-Set, sondern ein Type-Set beschreibt. Genau diese Erweiterung der Interface-Semantik macht Generics in Go möglich. Dieser Artikel arbeitet die Type-Set-Mechanik aus der Spec heraus, zeigt die beiden Built-ins any und comparable, führt durch das Paket cmp mit cmp.Ordered, erklärt das Tilde-Symbol ~T für das Matching von Underlying-Types und zeigt an zwei Praxis-Beispielen, wie eigene Constraints aussehen — von einem Numeric-Constraint für eine generische Summe bis zu einem Set[T comparable] mit Mengen-Operationen.

Was ein Constraint formal ist

Bevor wir Code schreiben, lohnt sich ein präziser Blick auf die Spec. Vor Go 1.18 war ein Interface ausschließlich ein Method-Set — eine Menge von Methoden, die ein konkreter Typ implementieren muss. Mit der Einführung von Generics wurde diese Definition erweitert: ein Interface beschreibt seither ein Type-Set, also die Menge aller konkreten Typen, die das Interface „erfüllen". Methoden bleiben der eine Weg, ein Type-Set einzuschränken; die neuen Type-Terms und Unions sind der andere.

Die Spec formuliert es in General interfaces sehr direkt:

The type set of a non-empty interface is the intersection of the type sets of its interface elements.

Heißt: jede Zeile eines Interfaces — eine Methode, ein Type-Term wie int, ein ~string, ein embedded Constraint — definiert für sich ein Type-Set. Das Gesamt-Constraint ist die Schnittmenge all dieser Sets. Genau das ist der mentale Schlüssel: ein Constraint ist nicht „eine Liste von Anforderungen", sondern eine Menge konkreter Typen, beschrieben durch das Interface.

Go was-ist-ein-constraint.go
package main

// Klassisches Interface — beschreibt das Method-Set { String() string }.
// Type-Set: alle konkreten Typen, die diese Methode haben.
type Stringer interface {
    String() string
}

// Generics-Constraint — beschreibt das Type-Set { int, int64, float64 }.
// Hier gibt es KEINE Methoden, nur erlaubte konkrete Typen.
type Numeric interface {
    int | int64 | float64
}

// Kombination — Type-Set { ~int }, deren Werte zusätzlich String() haben.
type StringableInt interface {
    ~int
    String() string
}

// Beide Interfaces sind syntaktisch dasselbe Konstrukt — der Compiler
// entscheidet aus dem Kontext, ob ein Interface als Variablen-Typ oder
// als Constraint verwendet wird. Type-Sets mit Type-Terms (int | ...)
// dürfen NUR als Constraint vorkommen, nicht als Variablen-Typ.

Drei Dinge zum Aufnehmen: Erstens, das Schlüsselwort für ein Constraint ist interface — es gibt keine separate Constraint-Syntax. Zweitens, die Type-Terms int | int64 | float64 sind eine Union: ein Typ erfüllt das Constraint, wenn er einer dieser Typen ist. Drittens, sobald ein Interface Type-Terms enthält, darf es ausschließlich als Constraint benutzt werden — nicht als var x Numeric für eine normale Variable. Die Spec sagt explizit: „Interfaces that are not basic may only be used as type constraints." Das ist die saubere Trennung zwischen Laufzeit-Interface (Method-Set) und Compile-Time-Constraint (Type-Set).

any — der weiteste Constraint

any ist seit Go 1.18 ein Alias für interface{} — das leere Interface, das jeden Typ aufnimmt. Als Constraint heißt das: jeder Typ ist erlaubt. Das Type-Set von any ist die Menge aller Nicht-Interface-Typen.

Diese maximale Freiheit hat einen Preis: innerhalb der generischen Funktion darfst du mit einem T any nicht viel machen. Du kannst den Wert speichern, kopieren, weitergeben, in eine Slice oder eine Map legen — aber du darfst ihn nicht mit == vergleichen, nicht mit < ordnen, nicht arithmetisch verrechnen. Operationen, die nur auf einer Untermenge der Typen funktionieren, sind verboten.

Go any-constraint.go
package main

import "fmt"

// Generischer Container — funktioniert mit jedem Typ.
// any reicht, weil wir den Wert nur speichern und zurückgeben.
type Box[T any] struct {
    value T
}

func NewBox[T any](v T) Box[T] {
    return Box[T]{value: v}
}

func (b Box[T]) Value() T {
    return b.value
}

// Eine generische Reverse-Funktion — braucht ebenfalls nur any,
// weil sie nur Indizes vertauscht, keine Werte vergleicht.
func Reverse[T any](xs []T) {
    for i, j := 0, len(xs)-1; i < j; i, j = i+1, j-1 {
        xs[i], xs[j] = xs[j], xs[i]
    }
}

func main() {
    b1 := NewBox(42)         // Box[int]
    b2 := NewBox("hallo")    // Box[string]
    fmt.Println(b1.Value(), b2.Value())

    xs := []float64{1.5, 2.5, 3.5, 4.5}
    Reverse(xs)
    fmt.Println(xs)
}
Output
42 hallo
[4.5 3.5 2.5 1.5]

Die Funktion Reverse führt nur Swap-Operationen aus — sie braucht keinerlei Wissen über die Werte, sie braucht nicht zu vergleichen, sie braucht nicht zu addieren. Deshalb passt any perfekt. Wann immer ein Algorithmus den Wert nur durchreicht (Container, Stack, Optional, Queue, lineare Suche mit Custom-Predicate als zweitem Argument), ist any das richtige Constraint. Erst wenn du Werte vergleichen oder ordnen willst, brauchst du etwas Schärferes — comparable oder cmp.Ordered.

comparable — alles, was mit == geht

Der zweite eingebaute Constraint ist comparable. Sein Type-Set ist nach der Spec die Menge aller Nicht-Interface-Typen, die strikt vergleichbar sind — also alle Typen, für die == und != ohne Laufzeit-Panic funktionieren. Dazu gehören:

  • alle Integer-, Float-, Complex-, String- und Bool-Typen,
  • Pointer und Channels (verglichen wird die Adresse),
  • Arrays mit vergleichbarem Element-Typ,
  • Structs, deren alle Felder vergleichbar sind,
  • Interfaces (mit Einschränkung — siehe unten).

Nicht vergleichbar sind dagegen Slices, Maps und Funktionen. Eine Slice mit == zu vergleichen ist ein Compile-Fehler; eine Map ebenso. Diese Typen sind also nicht im Type-Set von comparable.

Go comparable-constraint.go
package main

import "fmt"

// Index of — sucht das erste Vorkommen von target in xs.
// Wir brauchen ==, also reicht any NICHT, comparable ist Pflicht.
func IndexOf[T comparable](xs []T, target T) int {
    for i, v := range xs {
        if v == target {
            return i
        }
    }
    return -1
}

// Distinct — entfernt Duplikate. Braucht map[T]struct{} als Set —
// map-Keys müssen comparable sein, daher comparable als Constraint.
func Distinct[T comparable](xs []T) []T {
    seen := make(map[T]struct{}, len(xs))
    out := make([]T, 0, len(xs))
    for _, v := range xs {
        if _, ok := seen[v]; !ok {
            seen[v] = struct{}{}
            out = append(out, v)
        }
    }
    return out
}

func main() {
    fmt.Println(IndexOf([]string{"a", "b", "c"}, "b")) // 1
    fmt.Println(Distinct([]int{1, 2, 2, 3, 1, 4}))     // [1 2 3 4]
}
Output
1
[1 2 3 4]

Die Mechanik ist sauber: comparable ist genau dann notwendig, wenn du ==, != oder Map-Keys nutzt. Die Funktion IndexOf braucht nur ==; Distinct legt zusätzlich eine Map an, deren Keys das Type-Parameter T sind — Map-Keys müssen vergleichbar sein, also wieder comparable. Würdest du hier nur any verwenden, lehnt der Compiler die ==-Operation und den Map-Zugriff ab.

Eine wichtige Sprach-Entwicklung: bis Go 1.20 war ein Interface-Typ als Type-Argument für comparable nicht zugelassen — selbst dann nicht, wenn das Interface zur Laufzeit garantiert nur vergleichbare Werte enthielt. Das war eine pragmatische, aber unbequeme Einschränkung. Seit Go 1.20 erfüllen Interfaces den comparable-Constraint, mit dem expliziten Vorbehalt, dass == zwischen Interface-Werten mit nicht-vergleichbarem dynamischem Inhalt zur Laufzeit panicen kann. Wer auf Go 1.20+ entwickelt, kann sich diese Subtilität merken, sollte aber im Hinterkopf behalten: die Panik kommt erst zur Laufzeit, der Compiler warnt nicht mehr.

cmp.Ordered — alles, was mit < geht

comparable reicht für Gleichheit. Für Ordnung — die Operatoren <, <=, >, >= — brauchst du einen schärferen Constraint. Seit Go 1.21 liefert die Standard-Library dafür den Constraint cmp.Ordered im Paket cmp. Sein Type-Set sind alle ordnungsfähigen Typen der Sprache: die komplette Integer-Familie (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr), die Float-Familie (float32, float64) und string. Komplexe Zahlen sind nicht dabei — Komplexe haben in Go keine <-Operation.

Go ordered-constraint.go
package main

import (
    "cmp"
    "fmt"
)

// Min und Max — generisch für alle ordnungsfähigen Typen.
// cmp.Ordered erlaubt den < Operator innerhalb der Funktion.
func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Clamp — auf Range [lo, hi] einschränken.
func Clamp[T cmp.Ordered](v, lo, hi T) T {
    return Max(lo, Min(v, hi))
}

func main() {
    fmt.Println(Min(3, 7))          // 3
    fmt.Println(Max(2.5, 9.1))      // 9.1
    fmt.Println(Min("apple", "ax")) // apple
    fmt.Println(Clamp(15, 0, 10))   // 10
}
Output
3
9.1
apple
10

Zeile für Zeile: Min[T cmp.Ordered] legt einen Type-Parameter T fest, dessen Type-Set cmp.Ordered ist — der Aufrufer darf jeden Integer-, Float- oder String-Typ einsetzen. Innerhalb der Funktion ist a < b legal, weil jeder Typ im Type-Set diese Operation unterstützt. Würde der Aufrufer dagegen versuchen, Min(true, false) zu schreiben, lehnt der Compiler das ab — bool ist nicht in cmp.Ordered. Genau das ist der Punkt: ein scharfer Constraint dokumentiert und erzwingt, was die Funktion braucht.

Das Paket cmp liefert zusätzlich drei nützliche Funktionen, die alle auf cmp.Ordered aufbauen: cmp.Compare(x, y) gibt -1, 0 oder +1 zurück (klassische Drei-Wege-Comparison, ideal für slices.SortFunc), cmp.Less(x, y) gibt einen bool zurück und behandelt NaN deterministisch, und cmp.Or(vals ...T) (seit Go 1.22) gibt den ersten Nicht-Zero-Wert zurück — praktisch für Default-Werte und Coalesce-Logik. Damit ersetzt das Stdlib-Paket weitgehend das frühere golang.org/x/exp/constraints.

golang.org/x/exp/constraints — die alte Welt

Vor Go 1.21 gab es cmp.Ordered nicht. Wer einen Ordnungs-Constraint brauchte, hat ihn aus dem Paket golang.org/x/exp/constraints importiert. Das Paket existiert weiter und definiert sechs Constraints, die du bis heute in vielen Open-Source-Projekten findest:

ConstraintType-Set
constraints.Signed~int | ~int8 | ~int16 | ~int32 | ~int64
constraints.Unsigned~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
constraints.IntegerSigned | Unsigned
constraints.Float~float32 | ~float64
constraints.Complex~complex64 | ~complex128
constraints.Orderedseit 1.21 Alias für cmp.Ordered

Auffällig sind zwei Dinge. Erstens: jedes Constraint nutzt das Tilde-Symbol ~, damit nicht nur die Standard-Typen erlaubt sind, sondern auch eigene Typen mit dem entsprechenden Underlying-Type. Mehr dazu im nächsten Abschnitt. Zweitens: Integer ist als Union zweier embedded Constraints geschrieben (Signed | Unsigned), nicht als eine lange Liste. Embedded Constraints sind in der Type-Set-Algebra ganz normale Bausteine — das Type-Set der Union ist die Vereinigung der eingebetteten Sets.

Go constraints-paket.go
package main

import (
    "fmt"

    "golang.org/x/exp/constraints"
)

// Sum nur für Integer-Typen — kein Float, kein String, kein bool.
func Sum[T constraints.Integer](xs []T) T {
    var total T
    for _, v := range xs {
        total += v
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3, 4}))     // 10
    fmt.Println(Sum([]uint16{10, 20, 30}))  // 60
    // Sum([]float64{1.5, 2.5}) // Compile-Fehler: float64 nicht in Integer
}

Für neue Projekte ab Go 1.21 gilt: nimm cmp.Ordered aus der Stdlib statt constraints.Ordered aus dem x/exp-Paket. Für Integer-only, Signed-only oder Float-only-Constraints ist constraints noch der einzige Stdlib-nahe Weg — eine Migration in die offizielle Stdlib ist diskutiert, aber Stand 2026 nicht abgeschlossen. Wer den externen Import vermeiden will, schreibt das Constraint einfach inline; die sechs Definitionen sind keine zwei Dutzend Zeilen.

Das Tilde-Symbol ~T — Underlying-Type-Matching

Eine der wichtigsten Mechaniken im Constraint-System ist das Tilde-Symbol. Die Spec definiert es so:

The type set of a term of the form ~T is the set of all types whose underlying type is T.

Heißt: ~int matcht nicht nur den vordefinierten Typ int, sondern auch jeden Typ, dessen Underlying-Type int ist. Wer type UserID int schreibt, bekommt mit ~int automatisch Mitgliedschaft im Type-Set; mit nacktem int nicht.

Go tilde-vs-nackt.go
package main

import "fmt"

// Constraint OHNE Tilde — nur exakt int, float64.
type StrictNumeric interface {
    int | float64
}

// Constraint MIT Tilde — int, float64 UND alle Custom-Typen
// mit diesem Underlying-Type.
type LooseNumeric interface {
    ~int | ~float64
}

type UserID int       // Underlying: int
type Celsius float64  // Underlying: float64

func DoubleStrict[T StrictNumeric](v T) T { return v + v }
func DoubleLoose[T LooseNumeric](v T)   T { return v + v }

func main() {
    // Beide funktionieren mit Built-in-Typen.
    fmt.Println(DoubleStrict(7))     // 14
    fmt.Println(DoubleLoose(7))      // 14

    var u UserID = 42
    // DoubleStrict(u) // Compile-Fehler: UserID nicht in {int, float64}
    fmt.Println(DoubleLoose(u))      // 84

    var t Celsius = 20.5
    fmt.Println(DoubleLoose(t))      // 41
}
Output
14
14
84
41

Das ist die Mechanik in Aktion. DoubleStrict(u) wird abgelehnt, weil UserID zwar zuweisbar-kompatibel zu int ist, aber nicht identisch. DoubleLoose(u) funktioniert, weil ~int jeden Typ mit Underlying-Type int aufnimmt — und UserID ist genau das.

Die Praxis-Konsequenz ist groß: in fast allen idiomatischen Constraints findest du Tilde-Varianten, nicht die nackten Typen. cmp.Ordered, constraints.Integer, constraints.Float — alle benutzen ~. Der Grund: Go-Code arbeitet routinemäßig mit Domain-Typen wie UserID, OrderID, Cents, Pixels. Ohne Tilde würden generische Algorithmen diese Typen nicht akzeptieren — was den Sinn von Generics weitgehend aushebelt.

Es gibt eine Spec-Auflage: ~T ist nur dann legal, wenn T ein vordefinierter oder unaliasierter Typ ist, dessen Underlying-Type T selbst ist. Du darfst also ~int schreiben, aber nicht ~UserIDUserID ist selbst nicht sein eigener Underlying-Type. Damit verhindert die Sprache zirkuläre oder mehrdeutige Type-Sets.

Eigene Constraints definieren

Ein eigenes Constraint ist nichts anderes als ein Interface-Typ mit Type-Terms. Die Syntax ist identisch zu jedem anderen Interface, der Unterschied liegt nur darin, dass dieses Interface ausschließlich als Constraint verwendet wird.

Go eigene-constraints.go
package main

// Variante 1 — kompakte Union mit Tilde.
type Numeric interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
        ~float32 | ~float64
}

// Variante 2 — Composition aus eingebetteten Constraints.
// Lesbarer und wiederverwendbar.
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type Float interface {
    ~float32 | ~float64
}

type Number interface {
    Integer | Float
}

// Variante 3 — Constraint mit Methoden-Anforderung zusätzlich.
// Der Type-Parameter muss numerisch sein UND Stringer implementieren.
type PrintableNumber interface {
    Number
    String() string
}

Drei Varianten, drei Stilrichtungen. Variante 1 ist die direkteste Form — alles in einer Union, kein Hierarchie-Overhead. Variante 2 zerlegt das Constraint in semantische Bausteine; das ist die idiomatische Form, wenn du mehrere Constraints brauchst, die sich teilweise überlappen. Variante 3 zeigt die Komposition mit einer Methoden-Anforderung: das Type-Set ist die Schnittmenge aus „ist eine Zahl" und „hat String() string" — eine harte Anforderung, die in der Praxis selten erfüllt wird, aber demonstriert, dass Type-Terms und Methoden frei kombinierbar sind.

Worauf du beim Definieren achten musst, schreibt die Spec klar vor: Type-Terms in einer Union müssen paarweise disjunkt sein. int | ~int ist illegal — die Type-Sets überlappen. Ebenso darf eine Union keine Methoden enthalten und kein comparable direkt einschließen; Methoden- und comparable-Anteile gehören in eigene Zeilen des Interfaces.

Go constraint-praxis-min.go
package main

import "fmt"

type Number interface {
    ~int | ~int64 | ~float32 | ~float64
}

// Generischer Durchschnitt — funktioniert für jeden numerischen Typ.
func Average[T Number](xs []T) T {
    var sum T
    for _, v := range xs {
        sum += v
    }
    if len(xs) == 0 {
        return 0
    }
    return sum / T(len(xs))
}

type Cents int64

func main() {
    fmt.Println(Average([]int{2, 4, 6}))                  // 4
    fmt.Println(Average([]float64{1.5, 2.5, 3.5}))        // 2.5
    fmt.Println(Average([]Cents{1990, 2490, 3990}))       // 2823
}
Output
4
2.5
2823

Beachte den Wert von ~: ohne Tilde würde der Cents-Fall nicht kompilieren. Mit Tilde nimmt Average jeden Domain-Typ mit numerischem Underlying-Type entgegen — genau das ist die typische Anforderung in echter Anwendungslogik, wo Geldbeträge, IDs, Pixel-Koordinaten und ähnliches als eigene Typen modelliert sind.

Methoden in Constraints — wann das sinnvoll ist

Ein Constraint kann Methoden enthalten. Das ist genau dann nützlich, wenn die generische Funktion eine bestimmte Operation auf dem Wert braucht, die kein Sprach-Operator abdeckt — etwa eine Compare(other) int-Methode für eigene Vergleichslogik oder ein Clone() T für defensive Kopien.

Go methoden-constraint.go
package main

import "fmt"

// Constraint: Typ muss eine Compare-Methode haben.
// Der Type-Parameter selbst tritt als Argument der Methode auf.
type Comparable[T any] interface {
    Compare(other T) int
}

type Version struct {
    Major, Minor, Patch int
}

func (v Version) Compare(other Version) int {
    if v.Major != other.Major {
        return v.Major - other.Major
    }
    if v.Minor != other.Minor {
        return v.Minor - other.Minor
    }
    return v.Patch - other.Patch
}

// Generisches Sort-ähnliches Min — funktioniert mit jedem Typ,
// der Compare(self) int implementiert.
func MinBy[T Comparable[T]](a, b T) T {
    if a.Compare(b) <= 0 {
        return a
    }
    return b
}

func main() {
    v1 := Version{1, 2, 3}
    v2 := Version{1, 5, 0}
    fmt.Println(MinBy(v1, v2)) // {1 2 3}
}
Output
{1 2 3}

Hier passieren zwei interessante Dinge gleichzeitig. Erstens: der Constraint ist selbst generischComparable[T] bekommt einen Type-Parameter, der in der Methoden-Signatur wieder auftaucht. Zweitens: MinBy[T Comparable[T]] ist eine selbstbezügliche Form. T muss Comparable[T] erfüllen — also eine Compare(T) int-Methode haben, deren Argument wieder vom selben Typ ist. Das ist die in Go idiomatische Antwort auf F-bounded Polymorphism.

Eine wichtige Abgrenzung: ein Constraint mit nur Methoden ist semantisch äquivalent zu einem klassischen Interface — funktional könntest du genauso gut einen Interface-Parameter nehmen. Der Unterschied ist die Typsicherheit zur Compile-Zeit: bei func MinBy[T Comparable[T]](a, b T) T sind a und b desselben statischen Typs T; bei einer klassischen Interface-Variante wären die beiden Werte beliebige Implementierungen des Interfaces, der Compiler kann keinen gemeinsamen konkreten Typ erzwingen. Plus: der Rückgabe-Typ ist T, kein Interface-Wert — der Aufrufer bekommt den konkreten Typ zurück, ohne Type-Assertion.

Wann any reicht — wann nicht

Eine Frage, die in Code-Reviews regelmäßig auftaucht: welcher Constraint ist der richtige? Die kurze Antwort: nimm den schwächsten Constraint, der die Operationen im Funktions-Körper noch zulässt. Je schwächer das Constraint, desto mehr Aufrufer sind möglich.

Was die Funktion mit T tutMindest-Constraint
Nur speichern, weiterreichen, in Slice/Map mit anderem Key legenany
Mit == / != vergleichen, als Map-Key nutzencomparable
Mit < / <= / > / >= ordnencmp.Ordered
Arithmetik (+, -, *, /)eigene Number/Integer/Float-Constraint
Methode aufrufen (z. B. String(), Compare())Constraint mit Methoden-Set
Nur Integer-Bit-Operationen (&, |, ^, <<)eigene Integer-Constraint

Faustregel: wer den Constraint zu eng wählt, schließt Aufrufer aus; wer ihn zu weit wählt, kann den Body nicht implementieren. Die Praxis pendelt zwischen diesen beiden Fehlern. Ein typischer Anfänger-Fehler ist, vorsorglich comparable zu schreiben, obwohl die Funktion gar nicht vergleicht — das verbietet dem Aufrufer Slices und Maps als Type-Argument, ohne Gewinn. Umgekehrt ist es ein Fehler, eine Sum-Funktion mit any zu deklarieren — sie kompiliert dann gar nicht erst, weil + auf any nicht definiert ist.

Praxis 1 — generische Sum-Funktion über alle Zahlen

Die klassische Übung: schreibe eine Funktion, die die Summe aller Elemente einer Slice berechnet — und zwar für jeden numerischen Typ. Vor Generics brauchte man dafür entweder eine Funktion pro Typ oder eine Reflection-Lösung mit reflect.Value und Laufzeit-Type-Switches. Mit einem Number-Constraint wird das zur Drei-Zeilen-Funktion.

Go sum-generisch.go
package main

import "fmt"

// Number — Type-Set aller numerischen Typen, inklusive Domain-Typen
// (deshalb mit ~). Kein bool, kein string, kein complex.
type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64
}

// Sum — generisch über T Number.
// Innerhalb des Bodies ist + erlaubt, weil + auf allen Typen
// im Type-Set definiert ist (Intersection-Eigenschaft).
func Sum[T Number](xs []T) T {
    var total T // Zero-Value des konkreten Typs
    for _, v := range xs {
        total += v
    }
    return total
}

// SumProduct — generischer Bonus: Summe der elementweisen Produkte.
// Funktioniert nur, wenn beide Slices dieselbe Länge haben.
func SumProduct[T Number](a, b []T) T {
    n := len(a)
    if len(b) < n {
        n = len(b)
    }
    var total T
    for i := 0; i < n; i++ {
        total += a[i] * b[i]
    }
    return total
}

// Domain-Typ — z. B. ein Geld-Betrag in Cent.
type EUR int64

func main() {
    // Built-in-Typen
    fmt.Println(Sum([]int{1, 2, 3, 4, 5}))           // 15
    fmt.Println(Sum([]float64{1.1, 2.2, 3.3}))       // 6.6
    fmt.Println(Sum([]uint8{200, 50, 5}))            // 255

    // Domain-Typ — dank ~ in der Union erlaubt.
    prices := []EUR{1990, 2490, 3990}
    fmt.Println(Sum(prices))                          // 8470

    // Dot-Product für Vektoren
    xs := []float64{1, 2, 3}
    ys := []float64{4, 5, 6}
    fmt.Println(SumProduct(xs, ys))                   // 32 = 4 + 10 + 18
}
Output
15
6.6
255
8470
32

Was passiert hier konkret? Der Type-Parameter T wird beim Aufruf an einen konkreten Typ gebunden — Sum([]int{...}) instanziiert Sum[int], Sum([]EUR{...}) instanziiert Sum[EUR]. Innerhalb der instantiierten Funktion ist total T ein lokaler Wert vom Zielfunktyp; total += v wird vom Compiler in die typ-spezifische Addition übersetzt. Es gibt keine Laufzeit-Box wie bei Java-Generics, keine Reflection, keinen Type-Switch — der Compiler erzeugt entweder eine spezialisierte Version pro Typ (Monomorphisierung) oder eine GC-Shape-basierte Variante. In beiden Fällen ist total += v so schnell wie eine handgeschriebene typ-spezifische Funktion.

Ohne Generics hättest du diese Funktion entweder als interface{}-Variante mit Type-Switch oder als Code-Generator-Lösung schreiben müssen. Beide Wege kosten Performance, beide kosten Lesbarkeit. Der Constraint-basierte Generic-Code ist die saubere Antwort: ein Algorithmus, n Typen, voller Compile-Time-Check.

Praxis 2 — Set[T comparable] mit Mengen-Operationen

Das zweite Standard-Beispiel ist ein generisches Set. Go hat von Haus aus keinen Set-Typ; die idiomatische Lösung ist map[T]struct{}, weil Map-Lookups O(1) sind und struct{} null Byte belegt. Mit Generics packen wir das hinter eine saubere Schnittstelle.

Go set-comparable.go
package main

import "fmt"

// Set[T comparable] — generischer Mengen-Typ.
// Wir brauchen comparable, weil T als Map-Key dient.
type Set[T comparable] struct {
    data map[T]struct{}
}

// NewSet — Konstruktor mit optionalen Anfangs-Elementen.
func NewSet[T comparable](items ...T) *Set[T] {
    s := &Set[T]{data: make(map[T]struct{}, len(items))}
    for _, it := range items {
        s.data[it] = struct{}{}
    }
    return s
}

// Add fügt v hinzu (idempotent).
func (s *Set[T]) Add(v T) {
    s.data[v] = struct{}{}
}

// Has prüft Zugehörigkeit.
func (s *Set[T]) Has(v T) bool {
    _, ok := s.data[v]
    return ok
}

// Remove entfernt v (idempotent).
func (s *Set[T]) Remove(v T) {
    delete(s.data, v)
}

// Len gibt die Mächtigkeit.
func (s *Set[T]) Len() int {
    return len(s.data)
}

// Union — neue Menge mit allen Elementen aus s ODER other.
func (s *Set[T]) Union(other *Set[T]) *Set[T] {
    out := NewSet[T]()
    for v := range s.data {
        out.Add(v)
    }
    for v := range other.data {
        out.Add(v)
    }
    return out
}

// Intersection — Elemente, die in s UND other sind.
func (s *Set[T]) Intersection(other *Set[T]) *Set[T] {
    out := NewSet[T]()
    // Iteriere über die kleinere Menge — O(min(|s|, |other|)).
    small, big := s, other
    if other.Len() < s.Len() {
        small, big = other, s
    }
    for v := range small.data {
        if big.Has(v) {
            out.Add(v)
        }
    }
    return out
}

func main() {
    a := NewSet(1, 2, 3, 4)
    b := NewSet(3, 4, 5, 6)

    fmt.Println("|A|       =", a.Len())                    // 4
    fmt.Println("3 in A    =", a.Has(3))                   // true
    fmt.Println("|A ∪ B|   =", a.Union(b).Len())           // 6
    fmt.Println("|A ∩ B|   =", a.Intersection(b).Len())    // 2

    // Mit Strings.
    tags := NewSet("go", "rust", "zig")
    tags.Add("rust") // idempotent
    fmt.Println("|tags|    =", tags.Len()) // 3
}
Output
|A|       = 4
3 in A    = true
|A ∪ B|   = 6
|A ∩ B|   = 2
|tags|    = 3

Die zentrale Design-Entscheidung steckt im ersten Zeilen-Bereich: type Set[T comparable] struct { ... }. Der Constraint comparable ist hier nicht optional — er ist erzwungen durch die Implementierung. Eine Map in Go verlangt einen vergleichbaren Key-Typ, und sobald T Map-Key wird, muss das Constraint mindestens comparable sein. Würden wir hier any schreiben, würde der Compiler die Map-Deklaration map[T]struct{} mit der Fehlermeldung „type parameter T is not comparable with comparable types" ablehnen.

Die Folge dieses Constraints für den Aufrufer: er darf Set[int], Set[string], Set[UserID] bauen — aber nicht Set[[]int] oder Set[map[string]int], weil Slices und Maps selbst nicht vergleichbar sind. Das ist nicht eine künstliche Beschränkung, sondern eine direkte Konsequenz der Implementierungs-Wahl. Wer ein Set über nicht-vergleichbare Typen braucht, kommt um eine Hash-basierte Sonder-Implementierung mit explizitem Hash-Funktions-Parameter nicht herum.

Ein zweites Detail: NewSet[T]() ohne explizites Type-Argument geht in der Praxis nicht — der Compiler kann das T nicht aus dem leeren Argument inferrieren. Innerhalb der Methode Union siehst du daher die explizite Form NewSet[T](). Bei NewSet(1, 2, 3, 4) reicht dagegen die Inferenz, weil der Compiler T = int aus den Variadic-Argumenten ableiten kann. Constraint-Inference ist ein eigenes Thema und in der Spec ausführlich beschrieben; in der Praxis genügt die Faustregel: bei leerer Argumentliste expliziter Type-Parameter, sonst Inference.

Besonderheiten

Constraints sind Interfaces — aber nicht jedes Interface ist als Wert nutzbar.

Sobald ein Interface Type-Terms wie ~int | ~string enthält, ist es non-basic und darf laut Spec nur als Constraint verwendet werden. var x Numeric ist ein Compile-Fehler, wenn Numeric Type-Terms hat. Methoden-only-Interfaces bleiben dagegen normale Werte-Interfaces.

Tilde-Operator ~T ist fast immer richtig.

Wer einen Constraint ohne ~ schreibt, schließt Domain-Typen wie type UserID int aus. Solche Custom-Types sind in echtem Go-Code überall — Tilde ist in 95 % der Constraints die richtige Wahl. Nur wer absichtlich den Built-in-Typ erzwingen will, lässt das Tilde weg.

comparable ist seit Go 1.20 inklusiver geworden.

Bis Go 1.20 erfüllten Interface-Typen den comparable-Constraint nicht — selbst wenn ihr dynamischer Inhalt vergleichbar war. Seit 1.20 sind Interfaces erlaubt, mit dem Vorbehalt, dass ein == zwischen nicht-vergleichbaren dynamischen Werten zur Laufzeit panict. Praktischer, aber weniger compile-time-sicher.

cmp.Ordered ersetzt constraints.Ordered.

Seit Go 1.21 ist cmp.Ordered in der Stdlib. Neue Projekte sollten den Stdlib-Pfad nehmen; golang.org/x/exp/constraints.Ordered ist seither nur noch ein Alias auf den Stdlib-Constraint. Für Integer, Float, Signed, Unsigned ist das x/exp-Paket stand 2026 weiterhin der nächste Stdlib-nahe Weg.

Union-Terms müssen paarweise disjunkt sein.

int | ~int ist illegal — int ist Teil des Type-Sets von ~int, die Sets überlappen. Der Compiler lehnt das mit einer klaren Fehlermeldung ab. Beim Komponieren von Constraints aus Sub-Constraints (Signed | Unsigned) ist die Disjunktheit dagegen automatisch gegeben, weil signed und unsigned Integer keine gemeinsamen Typen haben.

Union mit Methoden geht nicht direkt.

Eine Zeile wie int | string | Stringer ist illegal — Unions dürfen keine Methoden-Interfaces enthalten. Wenn du beides willst (Type-Set UND Methode), schreibst du das in zwei Zeilen: erst die Union, dann die Methoden-Signatur. Das Type-Set des Gesamt-Interfaces ist dann die Schnittmenge.

Methoden-only-Constraints sind redundant — aber typsicherer.

Ein Constraint, das nur ein Method-Set ist, könnte man auch als klassisches Interface-Argument schreiben. Der Vorteil des Generic-Wegs: der Type-Parameter erhält den konkreten Typ, nicht einen Interface-Wert. Rückgabe-Typen sind konkret, kein Boxing, keine Type-Assertion beim Aufrufer.

Constraint zu eng oder zu weit — beide Fehler sind häufig.

Zu eng (comparable ohne Bedarf) schließt Aufrufer mit Slices oder Maps aus. Zu weit (any für eine Sum-Funktion) lässt den Body nicht kompilieren. Die Regel: wähle den schwächsten Constraint, der die im Body verwendeten Operationen noch erlaubt. Das maximiert die Wiederverwendbarkeit ohne Compile-Risiko.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Generics

Zur Übersicht