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 !
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 }
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 }
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.
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).
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.
func FunctionName[T TypeConstraint](parameterName T) ReturnType { // Function body using T }
func Sum[T int | float64](a, b T) T { return a + b }
Contraintes expérimentales
func PrintValues[T any](values []T) { for _, v := range values { fmt.Println(v) } }
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 }
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] }
La fonction Somme peut être appelée en utilisant uniquement les paramètres int, int64 et float64.
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 }
La contrainte Formatter requiert que tout type utilisé comme T doit avoir une méthode Format qui renvoie un chaîne.
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 }
Cette contrainte inclut à la fois des types spécifiques (int, float54) et nécessite la présence d'une méthode abs.
Avant l'introduction des génériques, l'interface{} était utilisée pour obtenir de la flexibilité. Cependant, cette approche a des limites.
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.
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.
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.
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 }
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 }
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é.
Les génériques produisent du code spécialisé pour chaque type, conduisant à de meilleures performances d'exécution par rapport à l'interface{}.
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.
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) } }
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.
Nous allons implémenter une file d'attente simple avec à la fois une interface{} et générique et comparer les résultats.
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 }
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] }
type Numeric interface { int | float64 | uint }
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.
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.
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.
Ces exemples présentent les concepts fondamentaux des génériques, vous aidant à comprendre la syntaxe et les fonctionnalités principales :
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!