Go Programming

Feuille de triche sur les génériques Go : 10 modèles de code pratiques pour les cartes, les tranches et les contraintes personnalisées

Depuis la sortie de Go 1.18, les génériques ont transformé le langage, permettant un code réutilisable et sûr sur le plan des types, sans la verbosité de interface{} ou de la réflexion. Pour les développeurs intermédiaires et avancés, maîtriser les génériques n'est plus une option, c'est essentiel pour écrire des applications Go idiomatiques, performantes et maintenables.

Cette feuille de triche condense 10 modèles essentiels couvrant les cas d'utilisation les plus courants : manipulation de tranches, gestion des cartes et exploitation des contraintes de types personnalisées. Que vous construisiez un pipeline de traitement de données ou un gestionnaire d'API complexe, ces modèles vous aideront à écrire un code Go plus propre.

1. La contrainte d'identité : paramètres de type de base

La forme la plus simple d'une fonction générique utilise l'alias `any` (qui est équivalent à `interface{}`) ou un paramètre de type contraint à un ensemble spécifique de types. C'est idéal lorsque vous devez retourner une valeur du même type que l'entrée.

func firstElement[T any](s []T) (T, bool) {
    var zero T
    if len(s) == 0 {
        return zero, false
    }
    return s[0], true
}

2. Contraintes numériques pour les opérations mathématiques

La bibliothèque standard de Go fournit des contraintes intégrées comme `~int` ou `~float64`. Le tilde (~) indique que le type sous-jacent doit correspondre au type entier ou flottant, vous permettant ainsi de travailler avec des types entiers personnalisés.

func SumNumbers[N int | float64](nums []N) N {
    var sum N
    for _, n := range nums {
        sum += n
    }
    return sum
}

3. Contraintes personnalisées avec des types d'union

Vous pouvez définir des interfaces personnalisées pour contraindre les paramètres de type à plusieurs types distincts. C'est puissant pour les fonctions qui doivent accepter à la fois des chaînes de caractères et des entiers.

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

func Max[T Number](a, b T) T {
    if a > b {
        return a
    }
    return b
}

4. Filtrage générique de tranches

Le filtrage est l'une des opérations les plus courantes sur les tranches. Une fonction de filtrage générique accepte une fonction prédicat qui retourne un booléen, permettant une logique hautement réutilisable.

func Filter[T any](items []T, predicate func(T) bool) []T {
    var result []T
    for _, item := range items {
        if predicate(item) {
            result = append(result, item)
        }
    }
    return result
}

// Utilisation :
// evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })

5. Fusion générique de cartes

Fusionner des cartes est délicat avec les génériques en raison des contraintes de clés. Vous pouvez créer une fonction générique qui fusionne deux cartes ayant les mêmes types de clés et de valeurs.

func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
    merged := make(map[K]V)
    for _, m := range maps {
        for k, v := range m {
            merged[k] = v
        }
    }
    return merged
}

6. Valeurs distinctes dans les tranches

Pour garantir l'unicité dans une tranche, vous pouvez utiliser une carte comme ensemble temporaire. Ce modèle nécessite que le paramètre de type `T` soit comparable.

func Distinct[T comparable](items []T) []T {
    seen := make(map[T]bool)
    var result []T
    for _, item := range items {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

7. Mappage générique de structures (Mappage d'objets)

Lors de la conversion entre différentes représentations de structures, les génériques vous permettent de mapper les champs en toute sécurité sans réflexion, à condition de définir la logique de transformation externement ou d'utiliser des bibliothèques utilitaires.

func MapStructs[T, U any](items []T, fn func(T) U) []U {
    result := make([]U, len(items))
    for i, item := range items {
        result[i] = fn(item)
    }
    return result
}

8. Accès sécurisé aux cartes avec des valeurs par défaut

L'accès aux clés d'une carte nécessite souvent de vérifier leur existence pour éviter les valeurs zéro. Une fonction générique peut retourner une valeur par défaut si la clé est manquante.

func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultVal V) V {
    if val, ok := m[key]; ok {
        return val
    }
    return defaultVal
}

9. Regroupement de tranches par clé

Regrouper les éléments d'une tranche dans des compartiments basés sur une fonction de clé est un modèle classique de programmation fonctionnelle. Cela nécessite que `K` soit comparable.

func GroupBy[K comparable, V any](items []V, keyFunc func(V) K) map[K][]V {
    groups := make(map[K][]V)
    for _, item := range items {
        key := keyFunc(item)
        groups[key] = append(groups[key], item)
    }
    return groups
}

10. Types pointeurs contraints

Parfois, vous devez travailler avec des pointeurs vers des structures. Vous pouvez contraindre les paramètres de type aux pointeurs en utilisant des interfaces ou une syntaxe spécifique, bien que cela soit moins courant que les types de valeur.

type PointerToStruct interface {
    *MyStruct
}

// Note : Les contraintes directes sur les pointeurs sont complexes ; généralement, vous contraignez le type sous-jacent
// et acceptez à la fois les pointeurs et les valeurs via des fonctions séparées ou une conception soignée.

Conclusion

Les génériques Go sont un outil puissant, mais ils nécessitent une application réfléchie. Les modèles ci-dessus montrent comment gérer des scénarios courants comme le filtrage, le mappage et le regroupement tout en maintenant la sécurité des types. Rappelez-vous que les génériques doivent simplifier le code, pas le compliquer. Commencez par ces dix modèles, expérimentez avec des contraintes personnalisées et vous verrez rapidement les avantages dans vos projets Go.

Share: