Contactez-nous

Utilisation des génériques dans le code Go

Apprenez à utiliser concrètement les génériques en Go : types paramétrés, fonctions génériques, instanciation, type inference, exemples de code et cas d'usage pour vos projets Go.

Types paramétrés (types génériques) en action : Créer des structures de données génériques

Les types paramétrés (types génériques) permettent de créer des structures de données génériques en Go, c'est-à-dire des structures de données (structs, interfaces, types dérivés) qui peuvent fonctionner avec différents types de données, sans duplication de code et en conservant le typage statique et la vérification de type à la compilation. L'utilisation de types paramétrés est particulièrement utile pour implémenter des collections génériques (piles, listes chaînées, arbres, maps génériques, etc.), des algorithmes génériques (tri générique, recherche générique, algorithmes de manipulation de collections génériques), et d'autres types de code générique réutilisable et flexible.

Exemples d'utilisation de types paramétrés (types génériques) pour créer des structures de données génériques :

  • Pile générique (Pile[T]) : Implémentation d'une pile (stack) générique qui peut stocker des éléments de n'importe quel type (grâce au type parameter T any). Le type paramétré Pile[T] permet de créer des piles d'entiers (Pile[int]), des piles de strings (Pile[string]), des piles de structs personnalisés (Pile[MonStruct]), ou des piles de n'importe quel autre type Go, en réutilisant le même code générique Pile[T] pour tous les types d'éléments.
    type Pile[T any] struct {
        elements []T
    }
    
    func (p *Pile[T]) Push(element T) {
        p.elements = append(p.elements, element)
    }
    
    func (p *Pile[T]) Pop() (T, bool) {
        if p.EstVide() {
            var zero T // Valeur zéro du type T
            return zero, false // Pile vide
        }
        element := p.elements[len(p.elements)-1]
        p.elements = p.elements[:len(p.elements)-1]
        return element, true
    }
    
    func (p *Pile[T]) EstVide() bool {
        return len(p.elements) == 0
    }
    
    var pileEntiers Pile[int] // Instanciation de Pile[T] avec le type 'int'
    pileEntiers.Push(10)
    pileEntiers.Push(20)
    elementInt, _ := pileEntiers.Pop()
    fmt.Println("Element retiré de la pile d'entiers :", elementInt)
    
    var pileStrings Pile[string] // Instanciation de Pile[T] avec le type 'string'
    pileStrings.Push("Go")
    pileStrings.Push("Generics")
    elementString, _ := pileStrings.Pop()
    fmt.Println("Element retiré de la pile de strings :", elementString)
    
  • Liste chaînée générique (ListeChainée[T]) : Implémentation d'une liste chaînée (linked list) générique qui peut stocker des éléments de n'importe quel type. Le type paramétré ListeChainée[T] et ses méthodes (Ajouter, Supprimer, Parcourir, etc.) peuvent être réutilisés pour créer des listes chaînées d'entiers, de strings, de structs, ou de tout autre type Go.
    type ListeChainee[T any] struct { // Liste chaînée générique
        valeur T
        suivant *ListeChainee[T]
    }
    
    func (l *ListeChainee[T]) Ajouter(valeur T) {
        // ... (logique d'ajout d'un élément à la liste chaînée générique) ...
    }
    
    func (l *ListeChainee[T]) Supprimer() T {
        // ... (logique de suppression d'un élément de la liste chaînée générique) ...
        var zero T
        return zero
    }
    
    func (l *ListeChainee[T]) Parcourir(f func(T)) {
        // ... (logique de parcours de la liste chaînée générique et appel de la fonction 'f' sur chaque élément) ...
    }
    
    var listeEntiers ListeChainee[int] // Instanciation de ListeChainee[T] avec le type 'int'
    listeEntiers.Ajouter(1)
    listeEntiers.Ajouter(2)
    listeEntiers.Parcourir(func(valeur int) {
        fmt.Println("Element de la liste d'entiers :", valeur)
    })
    
    var listeStrings ListeChainee[string] // Instanciation de ListeChainee[T] avec le type 'string'
    listeStrings.Ajouter("Generics")
    listeStrings.Ajouter("Go")
    listeStrings.Parcourir(func(valeur string) {
        fmt.Println("Element de la liste de strings :", valeur)
    })
    
  • Arbre binaire générique (ArbreBinaire[T constraints.Ordered]) : Implémentation d'un arbre binaire (binary tree) générique qui peut stocker des éléments de types ordonnés (grâce à la contrainte de type constraints.Ordered sur le type parameter T). Le type paramétré ArbreBinaire[T constraints.Ordered] et ses méthodes (Inserer, Rechercher, Parcourir, etc.) peuvent être réutilisés pour créer des arbres binaires d'entiers, de flottants, de strings, ou de tout autre type ordonné en Go.
    type ArbreBinaire[T constraints.Ordered] struct { // Arbre binaire générique avec type parameter 'T' contraint à 'constraints.Ordered'
        valeur T
        gauche  *ArbreBinaire[T]
        droite  *ArbreBinaire[T]
    }
    
    func (a *ArbreBinaire[T]) Inserer(valeur T) {
        // ... (logique d'insertion d'un élément dans l'arbre binaire générique) ...
    }
    
    func (a *ArbreBinaire[T]) Rechercher(valeur T) bool {
        // ... (logique de recherche d'un élément dans l'arbre binaire générique) ...
        return false
    }
    
    func (a *ArbreBinaire[T]) ParcourirInordre(f func(T)) {
        // ... (logique de parcours in-ordre de l'arbre binaire générique et appel de la fonction 'f' sur chaque élément) ...
    }
    
    var arbreEntiers ArbreBinaire[int] // Instanciation de ArbreBinaire[T] avec le type 'int'
    arbreEntiers.Inserer(5)
    arbreEntiers.Inserer(3)
    arbreEntiers.ParcourirInordre(func(valeur int) {
        fmt.Println("Element de l'arbre d'entiers :", valeur)
    })
    
    var arbreFlottants ArbreBinaire[float64] // Instanciation de ArbreBinaire[T] avec le type 'float64'
    arbreFlottants.Inserer(3.14)
    arbreFlottants.Inserer(2.71)
    arbreFlottants.ParcourirInordre(func(valeur float64) {
        fmt.Println("Element de l'arbre de flottants :", valeur)
    })
    

Ces exemples illustrent comment les types paramétrés (types génériques) permettent de créer des structures de données génériques en Go, réutilisables pour différents types de données, et offrant une alternative plus type-safe et plus performante que les approches basées sur l'interface vide interface{} pour la généricité.

Fonctions génériques en action : Algorithmes et opérations génériques

Les fonctions génériques permettent d'écrire des algorithmes génériques et des opérations génériques qui peuvent fonctionner avec différents types de données, en factorisant la logique algorithmique et en réduisant la duplication de code. L'utilisation de fonctions génériques est particulièrement utile pour implémenter des algorithmes de tri, de recherche, de manipulation de collections, ou d'autres opérations génériques qui peuvent s'appliquer à différents types de données.

Exemples d'utilisation de fonctions génériques pour créer des algorithmes et opérations génériques :

  • Fonction de tri générique (TrierSlice[T constraints.Ordered](s []T)) : Implémentation d'une fonction de tri générique qui peut trier un slice de n'importe quel type ordonné (grâce à la contrainte de type constraints.Ordered sur le type parameter T). La fonction TrierSlice[T] peut être réutilisée pour trier des slices d'entiers, de flottants, de strings, ou de tout autre type ordonné en Go, en utilisant le même code générique pour tous les types.
    import "sort"
    
    // Fonction de tri générique pour un slice de type ordonné 'T'
    func TrierSlice[T constraints.Ordered](s []T) {
        sort.Slice(s, func(i, j int) bool {
            return s[i] < s[j] // Utilisation de l'opérateur '<' (permis par la contrainte 'constraints.Ordered')
        })
    }
    
    sliceEntiers := []int{5, 2, 8, 1, 9, 4}
    TrierSlice[int](sliceEntiers) // Instanciation de TrierSlice[T] avec le type 'int'
    fmt.Println("Slice d'entiers trié :", sliceEntiers)
    
    sliceFlottants := []float64{3.14, 1.23, 2.71, 0.5}
    TrierSlice[float64](sliceFlottants) // Instanciation de TrierSlice[T] avec le type 'float64'
    fmt.Println("Slice de flottants trié :", sliceFlottants)
    
    sliceStrings := []string{"c", "a", "e", "b", "d"}
    TrierSlice[string](sliceStrings) // Instanciation de TrierSlice[T] avec le type 'string'
    fmt.Println("Slice de strings trié :", sliceStrings)
    
  • Fonction de recherche générique (RechercherElement[T comparable](s []T, element T) int) : Implémentation d'une fonction de recherche générique qui recherche un élément dans un slice de n'importe quel type comparable (grâce à la contrainte de type comparable sur le type parameter T). La fonction RechercherElement[T] peut être réutilisée pour rechercher des éléments dans des slices d'entiers, de strings, de structs comparables, ou de tout autre type comparable en Go.
    // Fonction de recherche générique pour un élément dans un slice de type comparable 'T'
    func RechercherElement[T comparable](s []T, element T) int {
        for i, v := range s {
            if v == element { // Utilisation de l'opérateur '==' (permis par la contrainte 'comparable')
                return i // Element trouvé, retourne l'index
            }
        }
        return -1 // Element non trouvé
    }
    
    sliceEntiers := []int{10, 20, 30, 40, 50}
    indexEntier := RechercherElement[int](sliceEntiers, 30) // Instanciation de RechercherElement[T] avec le type 'int'
    fmt.Println("Index de 30 dans sliceEntiers :", indexEntier) // Affiche 2
    
    sliceStrings := []string{"pomme", "banane", "orange"}
    indexString := RechercherElement[string](sliceStrings, "kiwi") // Instanciation de RechercherElement[T] avec le type 'string'
    fmt.Println("Index de \"kiwi\" dans sliceStrings :", indexString) // Affiche -1 (non trouvé)
    
  • Fonction de mapping générique (MapperSlice[T, U any](s []T, f func(T) U) []U) : Implémentation d'une fonction de mapping générique qui applique une fonction de transformation (passée en argument) à chaque élément d'un slice et retourne un nouveau slice contenant les résultats transformés. La fonction MapperSlice[T, U] est générique à la fois sur le type des éléments d'entrée (T any) et le type des éléments de sortie (U any), offrant une flexibilité maximale pour la transformation de slices de différents types de données.
    // Fonction de mapping générique pour transformer un slice de type 'T' en un slice de type 'U'
    func MapperSlice[T, U any](s []T, f func(T) U) []U {
        resultat := make([]U, len(s))
        for i, v := range s {
            resultat[i] = f(v) // Appel de la fonction de transformation 'f' sur chaque élément
        }
        return resultat
    }
    
    sliceEntiers := []int{1, 2, 3, 4, 5}
    sliceStrings := MapperSlice[int, string](sliceEntiers, func(n int) string {
        return fmt.Sprintf("Nombre : %d", n) // Fonction anonyme de transformation : int -> string
    })
    fmt.Println("Slice de strings mappé depuis sliceEntiers :", sliceStrings) // Affiche [Nombre : 1 Nombre : 2 Nombre : 3 Nombre : 4 Nombre : 5]
    
    sliceFlottants := MapperSlice[int, float64](sliceEntiers, func(n int) float64 {
        return float64(n) * 2.0 // Fonction anonyme de transformation : int -> float64
    })
    fmt.Println("Slice de flottants mappé depuis sliceEntiers :", sliceFlottants) // Affiche [2 4 6 8 10]
    

Ces exemples illustrent comment les fonctions génériques permettent d'écrire des algorithmes génériques et des opérations génériques en Go, réutilisables pour différents types de données, et offrant une alternative plus type-safe et plus performante que les approches basées sur l'interface vide interface{} pour la généricité des algorithmes.

Meilleures pratiques et cas d'utilisation appropriés pour les génériques

Pour utiliser les génériques de manière judicieuse et efficace dans vos projets Go, et éviter les pièges potentiels, voici quelques bonnes pratiques à suivre (rappelées du chapitre précédent, section "Bonnes pratiques pour l'utilisation des génériques en Go") :

Bonnes pratiques pour l'utilisation des génériques en Go : (rappel)

  • Utiliser les génériques uniquement lorsque c'est réellement justifié et nécessaire
  • Définir des contraintes de type claires et précises (interfaces)
  • Nommer les type parameters de manière concise et significative
  • Documenter clairement le code générique et les type parameters
  • Tester rigoureusement le code générique avec des tests unitaires complets
  • Benchmarker et profilez le code générique pour évaluer la performance (si la performance est critique)

Cas d'utilisation appropriés pour les génériques en Go :

Les génériques sont particulièrement adaptés à certains cas d'utilisation spécifiques en Go, où ils apportent une réelle valeur ajoutée en termes de réutilisabilité, de généricité, et de type-safety :

  • Structures de données génériques (collections génériques) : Implémenter des structures de données génériques (piles, listes chaînées, arbres, maps génériques, etc.) qui peuvent fonctionner avec différents types de données, en factorisant le code de la structure de données et en évitant la duplication de code pour chaque type de données spécifique. Les génériques permettent de créer des collections génériques type-safe, performantes, et faciles à utiliser et à réutiliser dans différents contextes.
  • Algorithmes génériques : Implémenter des algorithmes génériques (tri générique, recherche générique, algorithmes de manipulation de collections génériques, algorithmes mathématiques ou numériques génériques, etc.) qui peuvent fonctionner avec différents types de données compatibles, en factorisant la logique algorithmique et en évitant la duplication de code pour chaque type de données spécifique. Les génériques permettent de créer des algorithmes génériques type-safe, performants, et réutilisables pour différents types de données.
  • Fonctions utilitaires génériques : Créer des fonctions utilitaires génériques qui effectuent des opérations courantes ou répétitives sur différents types de données, en factorisant la logique utilitaire et en évitant la duplication de code pour chaque type de données spécifique. Les fonctions utilitaires génériques peuvent simplifier le code, améliorer la lisibilité, et faciliter la réutilisation de code pour les tâches utilitaires courantes.
  • Code de bas niveau et code d'infrastructure (avec prudence) : Dans certains cas très spécifiques et justifiés, les génériques peuvent être utilisés pour écrire du code de bas niveau ou du code d'infrastructure (par exemple, des bibliothèques de collections génériques, des frameworks génériques, des outils de métaprogrammation génériques, etc.) qui nécessitent une généricité et une flexibilité maximales, et où l'overhead de performance des génériques est acceptable ou négligeable par rapport aux avantages apportés en termes de réutilisabilité et de flexibilité. Cependant, l'utilisation de génériques dans le code de bas niveau ou le code d'infrastructure doit être faite avec prudence et responsabilité, en étant conscient des compromis potentiels en termes de complexité et de performance, et en testant rigoureusement le code générique pour garantir sa correction et sa performance dans tous les cas d'utilisation prévus.

En résumé, les génériques sont un outil puissant qui enrichit le langage Go et offre de nouvelles possibilités pour écrire du code plus générique, plus réutilisable, et plus flexible, en particulier pour les structures de données, les algorithmes, et les fonctions utilitaires génériques, dans les projets Go qui bénéficient de la généricité et de la réutilisabilité du code, et où les compromis potentiels des génériques (complexité, performance) sont acceptables ou négligeables par rapport aux avantages apportés.