Generics in der Programmiersprache Go

Generics ist ein wichtiges Feature, das in Go Version 1.18 eingeführt wurde und es Entwicklern ermöglicht, flexibleren und wiederverwendbareren Code zu schreiben.

Generics wird hauptsächlich durch die folgenden zwei Kernkonzepte umgesetzt:

  • Typparameter:Ermöglichen die Verwendung eines oder mehrerer Typen als Parameter in Funktions- oder Typdefinitionen.
  • Typbeschränkungen:Legt die Bedingungen fest, die Typparameter erfüllen müssen, und stellt sicher, dass mit diesen Typen innerhalb von Funktionen sicher gearbeitet werden kann.
Konzepte Funktion Beispiel
Typparameter Wird nach dem Funktions- oder Typnamen deklariert und steht für einen nicht angegebenen Typ. [T any]
Typbeschränkung Definiert die Bedingungen, die Typparameter erfüllen müssen (wie unterstützte Operatoren oder Methoden). int,float64,comparable,constraints.Ordered,any
any Typparameter aufanyTyp beschränken. [T any]
comparable Typparameter aufcomparableTyp beschränken. [K comparable]

Generics ermöglicht es uns, Code zu schreiben, der nicht von bestimmten Datentypen abhängt.

Vor der Einführung von Generics mussten wir, wenn wir verschiedene Datentypen verarbeiten wollten, normalerweise duplizierte Funktionen für jeden Typ schreiben.

Einschränkungen des traditionellen Ansatzes:

Beispiel

// Funktion für den int-Typ
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

// Funktion für den float64-Typ
func MaxFloat(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}

Lösung mit Generics:

Beispiel

// Eine Funktion verarbeitet mehrere Typen
func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Ausführliche Erklärung der Generics-Syntax

Deklaration von Typparametern

Generische Funktionen und Typen werden durch Typparameterlisten deklariert, mit der Syntax:[Type parameters Constraints]。

Beispiel

// Grundlegende Syntaxstruktur
funcFunktionsname[T-Beschränkung](Parameter T)Rückgabewerttyp{
    // Funktionskörper
}

typeTypname[T-Beschränkung] struct {
    // Struct-Feld
}

Namenskonventionen für Typparameter

  • Üblicherweise werden Großbuchstaben verwendet:T、K、V、Ewarten
  • T: steht für Typ
  • K: steht für Schlüssel
  • V: steht für Wert
  • E: steht für Element

Einschränkungen

Einschränkungen definieren die Bedingungen, die Typparameter erfüllen müssen, und sind ein Kernkonzept von Generics.

Eingebaute Constraints

1. anyConstraint

anyist ein leeres Interfaceinterface{}ein Alias für, was bedeutet, dass jeder Typ zulässig ist.

Beispiel

func PrintAny[T any](value T) {
    fmt.Printf("Wert: %v, Typ: %T\n", value, value)
}

// Anwendungsbeispiel
PrintAny(42)        // Wert: 42, Typ: int
PrintAny("hello")   // Wert: hello, Typ: string
PrintAny(3.14)      // Wert: 3.14, Typ: float64

2. comparableConstraint

comparableGibt Typunterstützung an==und!=Operator.

Beispiel

func FindIndex[T comparable](slice []T, target T) int {
    for i, v := range slice {
        if v == target {
            return i
        }
    }
    return -1
}

// Anwendungsbeispiel
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(FindIndex(numbers, 3))  // Ausgabe: 2

names := []string{"Alice", "Bob", "Charlie"}
fmt.Println(FindIndex(names, "Bob"))  // Ausgabe: 1

3. Union-Einschränkungen

Nutzung|Operator kombiniert mehrere Typen.

Beispiel

// Numerische Typ-Einschränkung
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

func Add[T Number](a, b T) T {
    return a + b
}

// Anwendungsbeispiel
fmt.Println(Add(10, 20))        // Ausgabe: 30
fmt.Println(Add(3.14, 2.71))    // Ausgabe: 5.85

Benutzerdefinierte Einschränkung

1. Methoden-Einschränkungen

Definieren Sie Einschränkungen, die bestimmte Methoden erfordern.

Beispiel

// Stringer-Einschränkung definieren
type Stringer interface {
    String() string
}

func PrintString[T Stringer](value T) {
    fmt.Println(value.String())
}

// Benutzerdefinierten Typ implementieren
type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d Jahre alt)", p.Name, p.Alter)
}

// Anwendungsbeispiel
person := Person{Name: "Alice", Age: 25}
PrintString(person)  // Ausgabe: Alice (25 Jahre alt)

2. Komplexe Einschränkungen

Kombiniere Typ- und Methodenanforderungen.

Beispiel

// Erfordert, dass der Typ numerisch ist und die String()-Methode implementiert
type NumericStringer interface {
    Number
    String() string
}

Übung zu generischen Funktionen

1. Generische Hilfsfunktionen

Beispiel

// Zwei Werte vertauschen
func Swap[T any](a, b T) (T, T) {
    return b, a
}

// Prüfen, ob das Slice ein Element enthält
func Contains[T comparable](slice []T, target T) bool {
    for _, item := range slice {
        if item == target {
            return true
        }
    }
    return false
}

// Funktion zum Deduplizieren
func Unique[T comparable](slice []T) []T {
    seen := make(map[T]bool)
    result := []T{}
   
    for _, item := range slice {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

// Anwendungsbeispiel
func main() {
    // Swap-Beispiel
    a, b := 10, 20
    a, b = Swap(a, b)
    fmt.Printf("a=%d, b=%d\n", a, b)  // Ausgabe: a=20, b=10
   
    // Contains-Beispiel
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(Contains(numbers, 3))  // Ausgabe: true
   
    // Unique-Beispiel
    duplicates := []int{1, 2, 2, 3, 4, 4, 5}
    unique := Unique(duplicates)
    fmt.Println(unique)  // Ausgabe: [1 2 3 4 5]
}

2. Mathematische Operationsfunktionen

Beispiel

// Finde den Maximalwert eines Slice
func Max[T Number](slice []T) T {
    if len(slice) == 0 {
        var zero T
        return zero
    }
   
    max := slice[0]
    for _, value := range slice[1:] {
        if value > max {
            max = value
        }
    }
    return max
}

// Finde den Minimalwert eines Slice
func Min[T Number](slice []T) T {
    if len(slice) == 0 {
        var zero T
        return zero
    }
   
    min := slice[0]
    for _, value := range slice[1:] {
        if value < min {
            min = value
        }
    }
    return min
}

// Ermittle den Durchschnittswert eines Slices
func Average[T Number](slice []T) float64 {
    if len(slice) == 0 {
        return 0
    }
   
    var sum T
    for _, value := range slice {
        sum += value
    }
    return float64(sum) / float64(len(slice))
}

// Anwendungsbeispiel
func main() {
    ints := []int{1, 5, 3, 9, 2}
    floats := []float64{1.1, 5.5, 3.3, 9.9, 2.2}
   
    fmt.Printf("Max int: %d\n", Max(ints))           // Ausgabe: 9
    fmt.Printf("Min float: %.1f\n", Min(floats))     // Ausgabe: 1.1
    fmt.Printf("Durchschnitt: %.2f\n", Average(floats))   // Ausgabe: 4.40
}

Generische Typen

1. Generische Strukturen

Beispiel

// Generische Stack-Implementierung
type Stack[T any] struct {
    Elemente []T
}

// Einfügen
func (s *Stack[T]) Push(value T) {
    s.elements = append(s.elements, value)
}

// Entfernen
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }
   
    lastIndex := len(s.elements) - 1
    value := s.elements[lastIndex]
    s.elements = s.elements[:lastIndex]
    return value, true
}

// Einen Blick auf das oberste Element werfen
func (s *Stack[T]) Peek() (T, bool) {
    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }
    return s.elements[len(s.elements)-1], true
}

// Überprüfen, ob der Stack leer ist
func (s *Stack[T]) IsEmpty() bool {
    return len(s.elements) == 0
}

// Anwendungsbeispiel
func main() {
    // Integer-Stack
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)
   
    fmt.Println(intStack.Pop())  // Ausgabe: 3 true
   
    // String-Stack
    stringStack := Stack[string]{}
    stringStack.Push("hello")
    stringStack.Push("world")
   
    fmt.Println(stringStack.Pop())  // Ausgabe: world true
}

2. Generische Map (Map)

Beispiel

// Threadsichere generische Map
type SafeMap[K comparable, V any] struct {
    data map[K]V
    mutex sync.RWMutex
}

// Neue SafeMap erstellen
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
    return &SafeMap[K, V]{
        data: make(map[K]V),
    }
}

// Schlüssel-Wert-Paar setzen
func (m *SafeMap[K, V]) Set(key K, value V) {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    m.data[key] = value
}

// Wert abrufen
func (m *SafeMap[K, V]) Get(key K) (V, bool) {
    m.mutex.RLock()
    defer m.mutex.RUnlock()
    value, exists := m.data[key]
    return value, exists
}

// Schlüssel löschen
func (m *SafeMap[K, V]) Delete(key K) {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    delete(m.data, key)
}

// Alle Schlüssel abrufen
func (m *SafeMap[K, V]) Keys() []K {
    m.mutex.RLock()
    defer m.mutex.RUnlock()
   
    keys := make([]K, 0, len(m.data))
    for key := range m.data {
        keys = append(keys⟦⟧key, key)
    }
    return keys
}

// Anwendungsbeispiel
func main() {
    // Eine String-zu-Integer-Map erstellen
    scores := NewSafeMap[string, int]()
    scores.Set(Alice", 95)
    scores.Set("Bob", 87)
   
    if score, exists := scores.Get("Alice"); existiert {
        fmt.Printf("Alice's score: %d\n", score)  // Ausgabe: Alice's score: 95
    }
   
    fmt.Println("Keys:", scores.Keys())  // Ausgabe: Keys: [Alice Bob]
}

Typinferenz

Der Go-Compiler kann Typparameter automatisch ableiten, wodurch der Code prägnanter wird.

Beispiel

// Der Typ muss nicht explizit angegeben werden
func main() {
    // Beispiel für Typinferenz
    fmt.Println(Max([]int{1, 2, 3}))        // Der Compiler leitet T als int ab
    fmt.Println(Max([]float64{1.1, 2.2}))   // Der Compiler leitet T als float64 ab
   
    // Typ explizit angeben (manchmal notwendig)
    var result int = Max[int]([]int{1, 2, 3})
    fmt.Println(result)
}

Übungsaufgaben

Übung 1: Implementiere einen generischen Filter

Schreibe eineFilterFunktion, die Slice-Elemente basierend auf einer Bedingung filtert.

Beispiel

// Hier gehört dein Implementierungscode hin
func Filter[T any](slice []T, predicate func(T) bool) []T {
    // Filterlogik implementieren
}

// Testcode
func main() {
    numbers := []int{1, 2, 3, 4, 5, 6}
    even := Filter(numbers, func(n int) bool {
        return n%2 == 0
    })
    fmt.Println(even)  // Sollte ausgeben: [2 4 6]
}

Übung 2: Implementiere eine generische map-Funktion

Schreibe eineMapEine Funktion, die jedes Element in einem Slice in einen anderen Typ konvertiert.

Beispiel

// Hier gehört dein Implementierungscode hin
func Map[T any, U any](slice []T, mapper func(T) U) []U {
    // Mapping-Logik implementieren
}

// Testcode
func main() {
    numbers := []int{1, 2, 3, 4, 5}
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("Number: %d", n)
    })
    fmt.Println(strings)
}

Häufige Probleme und Überlegungen

1. Performance-Überlegungen

Generics führen zur Kompilierzeit eine Typspezialisierung durch, wobei die Laufzeitperformance mit handgeschriebenem typspezifischem Code vergleichbar ist.

2. Wahl der Typbeschränkungen

  • Nutzunganyist am flexibelsten, wenn, aber in der Funktionalität eingeschränkt.
  • NutzungcomparableUnterstützt den Gleichheitsvergleich
  • Verwende Union-Constraints, um die verfügbaren konkreten Typen einzuschränken.

3. Fehlerbehandlung

Beispiel

// Gute Praktiken für die Fehlerbehandlung
func SafeMax[T Number](slice []T) (T, error) {
    if len(slice) == 0 {
        var zero T
        return zero, errors.New("slice is empty")
    }
    return Max(slice), nil
}
Weitere Erweiterungen