Génériques du langage Go

Les génériques sont une fonctionnalité importante introduite dans Go version 1.18, permettant aux développeurs d’écrire du code plus flexible et réutilisable.

Les génériques sont principalement mis en œuvre à travers les deux concepts fondamentaux suivants :

  • Paramètres de type :Permet d’utiliser un ou plusieurs types comme paramètres dans des définitions de fonctions ou de types.
  • Contraintes de type :Spécifie les conditions que les paramètres de type doivent satisfaire, garantissant que ces types peuvent être manipulés en toute sécurité à l’intérieur des fonctions.
Concepts Fonction Exemple
Paramètre de type Déclaré après le nom de la fonction ou du type, représentant un type non spécifié. [T any]
Contrainte de type Définit les conditions que les paramètres de type doivent satisfaire (comme les opérateurs ou méthodes pris en charge). int,float64,comparable,constraints.Ordered,any
any Contraindre les paramètres de type au typeany. [T any]
comparable Contraindre les paramètres de type au typecomparable. [K comparable]

Les génériques nous permettent d’écrire du code qui ne dépend pas de types de données spécifiques.

Avant l'introduction des génériques, si nous voulions traiter différents types de données, nous devions généralement écrire des fonctions dupliquées pour chaque type.

Limites de l'approche traditionnelle :

Exemple

// Fonction pour gérer le type int
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

// Fonction pour gérer le type float64
func MaxFloat(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}

Solution utilisant les génériques :

Exemple

// Une fonction gère plusieurs types
func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Explication détaillée de la syntaxe des génériques

Déclaration des paramètres de type

Les fonctions et types génériques sont déclarés au moyen de listes de paramètres de type, avec la syntaxe :[Type parameters Constraints]。

Exemple

// Structure syntaxique de base
funcNom de la fonction[Contrainte T](paramètre T)Type de la valeur de retour{
    // Corps de la fonction
}

typenom du type[Contrainte T] struct {
    // Champ de structure
}

Conventions de nommage des paramètres de type

  • Habituellement, des lettres majuscules sont utilisées :T、K、V、Ewait
  • T: désigne le type
  • K: indique la clé
  • V: indique la valeur
  • E: désigne l'élément

Contraintes

Les contraintes définissent les conditions que les paramètres de type doivent satisfaire et constituent un concept fondamental des génériques.

Contraintes intégrées

1. anycontrainte

anyest une interface videinterface{}un alias de, ce qui signifie que tout type est autorisé.

Exemple

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

// Exemple d'utilisation
PrintAny(42)        // Valeur : 42, Type : int
PrintAny("hello")   // Valeur : hello, Type : string
PrintAny(3.14)      // Valeur : 3.14, Type : float64

2. comparablecontrainte

comparableIndique la prise en charge du type==et!=operator.

Exemple

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

// Exemple d'utilisation
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(FindIndex(numbers, 3))  // Sortie : 2

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

3. Contraintes d'union

Utilisation|L'opérateur combine plusieurs types.

Exemple

// Contrainte de type numérique
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
}

// Exemple d'utilisation
fmt.Println(Add(10, 20))        // Sortie : 30
fmt.Println(Add(3.14, 2.71))    // Sortie : 5.85

Contrainte personnalisée

1. Contraintes de méthode

Définissez des contraintes qui exigent des méthodes spécifiques.

Exemple

// Définir la contrainte Stringer
type Stringer interface {
    String() string
}

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

// Implémenter un type personnalisé
type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d ans)", p.Name, p.Âge)
}

// Exemple d'utilisation
person := Person{Name: "Alice", Age: 25}
PrintString(person)  // Sortie : Alice (25 ans)

2. Contraintes complexes

Combinez les exigences de type et de méthode.

Exemple

// Exige que le type soit numérique et qu'il implémente la méthode String()
type NumericStringer interface {
    Number
    String() string
}

Pratique des fonctions génériques

1. Fonctions utilitaires génériques

Exemple

// Échanger deux valeurs
func Swap[T any](a, b T) (T, T) {
    return b, a
}

// Vérifier si la slice contient un élément
func Contains[T comparable](slice []T, target T) bool {
    for _, item := range slice {
        if item == target {
            return true
        }
    }
    return false
}

// Fonction de déduplication
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
}

// Exemple d'utilisation
func main() {
    // Exemple d'échange
    a, b := 10, 20
    a, b = Swap(a, b)
    fmt.Printf("a=%d, b=%d\n", a, b)  // Sortie : a=20, b=10
   
    // Exemple de Contains
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(Contains(numbers, 3))  // Sortie : true
   
    // Exemple de Unique
    duplicates := []int{1, 2, 2, 3, 4, 4, 5}
    unique := Unique(duplicates)
    fmt.Println(unique)  // Sortie : [1 2 3 4 5]
}

2. Fonctions d'opérations mathématiques

Exemple

// Trouver la valeur maximale d'une tranche
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
}

// Trouver la valeur minimale d'une 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
}

// Trouver la valeur moyenne d'une tranche
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))
}

// Exemple d'utilisation
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))           // Sortie : 9
    fmt.Printf("Min float: %.1f\n", Min(floats))     // Sortie : 1.1
    fmt.Printf("Moyenne : %.2f\n", Moyenne(floats))   // Sortie : 4.40
}

Types génériques

1. Structures génériques

Exemple

// Implémentation d'une pile générique
type Stack[T any] struct {
    éléments []T
}

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

// Dépiler
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
}

// Consulter l'élément au sommet
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
}

// Vérifier si la pile est vide
func (s *Stack[T]) IsEmpty() bool {
    return len(s.elements) == 0
}

// Exemple d'utilisation
func main() {
    // Pile d'entiers
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)
   
    fmt.Println(intStack.Pop())  // Sortie : 3 true
   
    // Pile de chaînes
    stringStack := Stack[string]{}
    stringStack.Push("hello")
    stringStack.Push("world")
   
    fmt.Println(stringStack.Pop())  // Sortie : world true
}

2. Table générique (Map)

Exemple

// Table générique sécurisée pour les threads
type SafeMap[K comparable, V any] struct {
    data map[K]V
    mutex sync.RWMutex
}

// Créer une nouvelle SafeMap
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
    return &SafeMap[K, V]{
        data: make(map[K]V),
    }
}

// Définir la paire clé-valeur
func (m *SafeMap[K, V]) Set(key K, value V) {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    m.data[key] = value
}

// Obtenir la valeur
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
}

// Supprimer la clé
func (m *SafeMap[K, V]) Delete(key K) {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    delete(m.data, key)
}

// Obtenir toutes les clés
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)
    }
    return keys
}

// Exemple d'utilisation
func main() {
    // Créer une map de chaînes vers entiers
    scores := NewSafeMap[string, int]()
    scores.Set("Alice", 95)
    scores.Set("Bob", 87)
   
    if score, exists := scores.Get("Alice"); il existe {
        fmt.Printf("Score d'Alice : %d"\n", score)  // Sortie : Score d'Alice : 95
    }
   
    fmt.Println("Clés :", scores.Keys())  // Sortie : Clés : [Alice Bob]
}

Inférence de type

Le compilateur Go peut inférer automatiquement les paramètres de type, ce qui rend le code plus concis.

Exemple

// Pas besoin de spécifier explicitement le type
func main() {
    // Exemple d'inférence de type
    fmt.Println(Max([]int{1, 2, 3}))        // Le compilateur infère T comme int
    fmt.Println(Max([]float64{1.1, 2.2}))   // Le compilateur infère T comme float64
   
    // Spécifier explicitement le type (parfois nécessaire)
    var result int = Max[int]([]int{1, 2, 3})
    fmt.Println(result)
}

Exercices pratiques

Exercice 1 : Implémenter un filtre générique

Écrivez uneFilterfonction qui filtre les éléments d'une slice selon une condition.

Exemple

// Insérez votre code d'implémentation ici
func Filter[T any](slice []T, predicate func(T) bool) []T {
    // Implémenter la logique de filtrage
}

// Code de test
func main() {
    numbers := []int{1, 2, 3, 4, 5, 6}
    even := Filter(numbers, func(n int) bool {
        return n%2 == 0
    })
    fmt.Println(even)  // Devrait afficher : [2 4 6]
}

Exercice 2 : Implémenter une fonction map générique

Écrivez uneMapUne fonction qui convertit chaque élément d'une slice en un autre type.

Exemple

// Insérez votre code d'implémentation ici
func Map[T any, U any](slice []T, mapper func(T) U) []U {
    // Implémenter la logique de mappage
}

// Code de test
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)
}

Problèmes courants et considérations

1. Considérations sur les performances

Les génériques effectuent une spécialisation de type à la compilation, avec des performances d'exécution comparables à celles d'un code spécifique écrit à la main.

2. Choix des contraintes de type

  • Utilisationanyest le plus flexible lorsque, mais limité en fonctionnalités.
  • UtilisationcomparablePrend en charge la comparaison d'égalité
  • Utilisez des contraintes d'union pour restreindre les types concrets disponibles.

3. Gestion des erreurs

Exemple

// Bonnes pratiques de gestion des erreurs
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
}
autres extensions