Maison > développement back-end > Golang > Generics in Go : transformer la réutilisabilité du code

Generics in Go : transformer la réutilisabilité du code

Patricia Arquette
Libérer: 2025-01-08 06:20:41
original
494 Les gens l'ont consulté

Les génériques, introduits dans Go 1.18, ont révolutionné la manière d'écrire du code réutilisable et de type sécurisé. Les génériques apportent flexibilité et puissance tout en conservant la philosophie de simplicité de Go. Cependant, comprendre les nuances, les avantages et la façon dont les génériques se comparent aux approches traditionnelles (comme interface{} ) nécessite un examen plus approfondi.

Explorons les subtilités des génériques, approfondissons les contraintes, comparons les génériques à l'interface{} et démontrons leurs applications pratiques. Nous aborderons également les considérations de performances et les implications en matière de taille binaire. Allons-y !

Qu’est-ce que les génériques ?

Les génériques permettent aux développeurs d'écrire des fonctions et des structures de données pouvant fonctionner sur n'importe quel type tout en maintenant la sécurité des types. Au lieu de s'appuyer sur interface{}, qui implique des assertions de type au moment de l'exécution, les génériques vous permettent de spécifier un ensemble de contraintes qui dictent les opérations autorisées sur les types.

Syntaxe

func FunctionName[T TypeConstraint](parameterName T) ReturnType {
    // Function body using T
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

T : Un paramètre de type, représentant un espace réservé pour le type.

TypeConstraint : restreint le type de T à un type spécifique ou à un ensemble de types.

parameterName T : Le paramètre utilise le type générique T.

ReturnType : La fonction peut également renvoyer une valeur de type T.

Exemple

func Sum[T int | float64](a, b T) T {
    return a + b
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

func Sum : Déclare le nom de la fonction, Sum

[T int | float64] : Spécifie une liste de paramètres de type qui introduit T comme paramètre de type, limité à des types spécifiques (int ou float64). La fonction Somme ne peut prendre que des paramètres int ou float64, pas en combinaison, les deux doivent être soit int, soit float64. Nous explorerons cela plus en détail dans les sections ci-dessous.

(a, b T): Déclare deux paramètres, a et b, tous deux de type T (le type générique ).

T : Spécifie le type de retour de la fonction, qui correspond au paramètre de type T.

Contraintes : éléments constitutifs des génériques

Les contraintes définissent quelles opérations sont valides pour un type générique. Go fournit des outils puissants pour les contraintes, y compris le package de contraintes expérimentales (golang.org/x/exp/constraints).

Contraintes intégrées

Go a introduit des contraintes intégrées avec les génériques pour assurer la sécurité des types tout en permettant une flexibilité dans la définition de code réutilisable et générique. Ces contraintes permettent aux développeurs d'appliquer des règles sur les types utilisés dans les fonctions ou types génériques.

Go a des contraintes intégrées ci-dessous

  1. any : représente n'importe quel type. C'est un alias pour interface{}. Ceci est utilisé lorsqu'aucune contrainte n'est nécessaire
func FunctionName[T TypeConstraint](parameterName T) ReturnType {
    // Function body using T
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
  1. comparable : autorise les types qui prennent en charge la comparaison d'égalité (== et !=). Utile pour les clés de cartes, la détection des doublons ou les contrôles d'égalité. Cela ne peut pas être utilisé pour les cartes, les tranches et les fonctions, car ces types ne prennent pas en charge la comparaison directe.
func Sum[T int | float64](a, b T) T {
    return a + b
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Contraintes expérimentales

  1. constraints.Complex : autorise les types numériques complexes (complex64 et complex128).
  2. constraints.Float : autorise les types numériques float (float32 et float64)
  3. constraints.Integer : autorise tout entier signé et non signé (int8, int16, int32, int64, int, uint8, uint16, uint32, uint64 et uint)
  4. constraints.Signed : autorise tout entier signé (int8, int16, int32, int64 et int)
  5. constraints.Unsigned : autorise tout entier non signé (uint8, uint16, uint32, uint64 et uint).
  6. constraint.Ordered : autorise les types qui permettent la comparaison (<. <=, >, >=), tous les types numériques et chaînes sont pris en charge (int, float64, string, etc.).
func PrintValues[T any](values []T) {
    for _, v := range values {
        fmt.Println(v)
    }
}
Copier après la connexion
Copier après la connexion

Contraintes personnalisées

Les contraintes personnalisées sont des interfaces qui définissent un ensemble de types ou de comportements de type qu'un paramètre de type générique doit satisfaire. En créant vos propres contraintes, nous pouvons ;

  • Limiter les types à un sous-ensemble spécifique, tel que les types numériques.

  • Exiger des types pour implémenter des méthodes ou des comportements spécifiques.

  • Ajoutez plus de contrôle et de spécificité à vos fonctions et types génériques.

Syntaxe

func CheckDuplicates[T comparable](items []T) []T {
    seen := make(map[T]bool)
    duplicates := []T{}
    for _, item := range items {
        if seen[item] {
            duplicates = append(duplicates, item)
        } else {
            seen[item] = true
        }
    }
    return duplicates
}
Copier après la connexion
Copier après la connexion

Exemple

import (
     "golang.org/x/exp/constraints"
     "fmt"
)

func SortSlice[T constraints.Ordered](items []T) []T {
    sorted := append([]T{}, items...) // Copy slice
    sort.Slice(sorted, func(i, j int) bool {
        return sorted[i] < sorted[j]
    })
    return sorted
}

func main() {
    nums := []int{5, 2, 9, 1}
    fmt.Println(SortSlice(nums)) // Output: [1 2 5 9]

    words := []string{"banana", "apple", "cherry"}
    fmt.Println(SortSlice(words)) // Output: [apple banana cherry]
}
Copier après la connexion
Copier après la connexion

La fonction Somme peut être appelée en utilisant uniquement les paramètres int, int64 et float64.

Contraintes par méthode

Si vous souhaitez imposer qu'un type doive implémenter certaines méthodes, vous pouvez le définir en utilisant ces méthodes.

type Numeric interface {
    int | float64 | uint
}
Copier après la connexion
Copier après la connexion

La contrainte Formatter requiert que tout type utilisé comme T doit avoir une méthode Format qui renvoie un chaîne.

Combinaison de contraintes

Les contraintes personnalisées peuvent combiner des ensembles de types et des exigences de méthode

type Number interface {
    int | int64 | float64
}

func Sum[T Number](a, b T) T {
    return a + b
}
Copier après la connexion

Cette contrainte inclut à la fois des types spécifiques (int, float54) et nécessite la présence d'une méthode abs.

Génériques vs interface{}

Avant l'introduction des génériques, l'interface{} était utilisée pour obtenir de la flexibilité. Cependant, cette approche a des limites.

Sécurité

  • interface{} : s'appuie sur des assertions de type d'exécution, augmentant le risque d'erreurs au moment de l'exécution.

  • Génériques : offre une sécurité de type au moment de la compilation, détectant les erreurs dès le début du développement.

Performance

  • interface{} : plus lente en raison de vérifications de type d'exécution supplémentaires.

  • Génériques : plus rapide, car le compilateur génère des chemins de code optimisés spécifiques aux types.

Lisibilité du code

  • interface{} : souvent verbeuse et moins intuitive, ce qui rend le code plus difficile à maintenir.

  • Génériques : une syntaxe plus propre conduit à un code plus intuitif et maintenable.

Taille binaire

  • interface{} : génère des binaires plus petits car elle ne duplique pas le code pour différents types.

  • Génériques : augmente légèrement la taille du binaire en raison de la spécialisation du type pour de meilleures performances.

Exemple

func FunctionName[T TypeConstraint](parameterName T) ReturnType {
    // Function body using T
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Le code fonctionne bien, l'assertion de type est une surcharge. La fonction Add peut être appelée avec n'importe quel argument, les paramètres a et b peuvent être de types différents, mais le code plantera lors de l'exécution.

func Sum[T int | float64](a, b T) T {
    return a + b
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Les génériques éliminent le risque de panique d'exécution causée par des assertions de type incorrectes et améliorent la clarté.

Performance

Les génériques produisent du code spécialisé pour chaque type, conduisant à de meilleures performances d'exécution par rapport à l'interface{}.

Taille binaire

Un compromis existe : les génériques augmentent la taille des binaires en raison de la duplication de code pour chaque type, mais cela est souvent négligeable par rapport aux avantages.

Limites des génériques Go

Complexité des contraintes : Même si les contraintes ressemblent à des contraintes.Ordonnées, elles simplifient les cas d'utilisation courants, la définition de contraintes hautement personnalisées peut devenir verbeuse.

Aucune inférence de type dans les structures : Contrairement aux fonctions, vous devez spécifier explicitement le paramètre de type pour les structures.

func PrintValues[T any](values []T) {
    for _, v := range values {
        fmt.Println(v)
    }
}
Copier après la connexion
Copier après la connexion

Limité aux contraintes de temps de compilation : Les génériques Go se concentrent sur la sécurité au moment de la compilation, tandis que des langages comme Rust offrent des contraintes plus puissantes utilisant des durées de vie et des traits.

Faisons un benchmark – Mieux fait que dit

Nous allons implémenter une file d'attente simple avec à la fois une interface{} et générique et comparer les résultats.

Interface{} Implémentation de la file d'attente

func CheckDuplicates[T comparable](items []T) []T {
    seen := make(map[T]bool)
    duplicates := []T{}
    for _, item := range items {
        if seen[item] {
            duplicates = append(duplicates, item)
        } else {
            seen[item] = true
        }
    }
    return duplicates
}
Copier après la connexion
Copier après la connexion

Implémentation de file d'attente générique

import (
     "golang.org/x/exp/constraints"
     "fmt"
)

func SortSlice[T constraints.Ordered](items []T) []T {
    sorted := append([]T{}, items...) // Copy slice
    sort.Slice(sorted, func(i, j int) bool {
        return sorted[i] < sorted[j]
    })
    return sorted
}

func main() {
    nums := []int{5, 2, 9, 1}
    fmt.Println(SortSlice(nums)) // Output: [1 2 5 9]

    words := []string{"banana", "apple", "cherry"}
    fmt.Println(SortSlice(words)) // Output: [apple banana cherry]
}
Copier après la connexion
Copier après la connexion
type Numeric interface {
    int | float64 | uint
}
Copier après la connexion
Copier après la connexion

Analyse des résultats

  • Durée d'exécution :
    L'implémentation générique est environ 63,64 % plus rapide que la version interface{} car elle évite les assertions de type d'exécution et fonctionne directement sur le type donné.

  • Allocations :
    La version interface{} effectue 3 fois plus d'allocations, principalement en raison du boxing/unboxing lors de l'insertion et de la récupération de valeurs. Cela ajoute des frais généraux à la collecte des ordures.

Pour les charges de travail plus importantes, telles qu'un million d'opérations de mise en file d'attente/retrait de la file d'attente, l'écart de performances se creuse. Les applications du monde réel ayant des exigences de débit élevé (par exemple, files d'attente de messages, planificateurs de tâches) bénéficient considérablement des génériques.

Pensées finales

Generics in Go établit un équilibre entre puissance et simplicité et offre une solution pratique pour écrire du code réutilisable et sécurisé. Bien qu'il ne soit pas aussi riche en fonctionnalités que Rust ou C, il s'aligne parfaitement sur la philosophie minimaliste de Go. Comprendre les contraintes telles que les contraintes. Commander et exploiter efficacement les génériques peut grandement améliorer la qualité et la maintenabilité du code.

À mesure que les génériques continuent d’évoluer, ils sont destinés à jouer un rôle central dans l’écosystème de Go. Alors plongez, expérimentez et adoptez la nouvelle ère de sécurité des types et de flexibilité dans la programmation Go !

Consultez le référentiel github pour quelques exemples sur les génériques.

GitHub logo sadananddodawadakar / AllerGénériques

Le référentiel contient des exemples fonctionnels de génériques go

Go Generics : référentiel d'exemples complet

Bienvenue dans le Référentiel Go Generics ! Ce référentiel est une ressource unique pour comprendre, apprendre et maîtriser les génériques dans Go, introduits dans la version 1.18. Les génériques apportent la puissance des paramètres de type à Go, permettant aux développeurs d'écrire du code réutilisable et sécurisé sans compromettre les performances ou la lisibilité.

Ce référentiel contient des exemples soigneusement sélectionnés qui couvrent un large éventail de sujets, de la syntaxe de base aux modèles avancés et aux cas d'utilisation pratiques. Que vous soyez un développeur Go débutant ou expérimenté, cette collection vous aidera à exploiter efficacement les génériques dans vos projets.


? Qu'y a-t-il à l'intérieur

? Programmes génériques de base

Ces exemples présentent les concepts fondamentaux des génériques, vous aidant à comprendre la syntaxe et les fonctionnalités principales :

  1. GenericMap : démontre une fonction de carte générique pour transformer des tranches de tout type.
  2. Swap : Un exemple simple mais puissant d'échange générique de deux valeurs.
  3. FilterSlice : montre comment filtrer…


Voir sur GitHub


Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal