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
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
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
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 TypK: steht für SchlüsselV: steht für WertE: 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
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
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
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
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
type NumericStringer interface {
Number
String() string
}
Übung zu generischen Funktionen
1. Generische Hilfsfunktionen
Beispiel
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
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
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
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
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
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
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
- Nutzung
anyist am flexibelsten, wenn, aber in der Funktionalität eingeschränkt. - Nutzung
comparableUnterstützt den Gleichheitsvergleich - Verwende Union-Constraints, um die verfügbaren konkreten Typen einzuschränken.
3. Fehlerbehandlung
Beispiel
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
}