ProgrammationDéveloppeur Backend

Comment les généricités (generics) sont-elles implémentées en Go ? Quelles sont les limitations, la syntaxe, les pièges à éviter et les cas d'utilisation appropriés ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Les généricités ou generics ont été introduites dans Go à partir de la version 1.18. Pendant longtemps, Go a été considéré comme un langage conservateur, où, pour des raisons de simplicité, les generics ont été exclus, mais avec l'augmentation du nombre de projets et l'évolution de l'écosystème, il est devenu nécessaire d'écrire des fonctions et des structures universelles. Cela est particulièrement critique pour les structures de conteneurs, les algorithmes de traitement de collections et le code d'infrastructure.

Problème : Avant l'apparition des generics, il était nécessaire de dupliquer le code ou d'utiliser des interfaces vides (interface{}), ce qui entraînait une perte de sécurité de type et une diminution des performances, ainsi qu'une complexité accrue pour le débogage.

Solution : Les generics sont implémentés en Go à l'aide de paramètres de type, qui sont spécifiés entre crochets pour les fonctions et les types. Avec les contraintes, il est possible de restreindre les paramètres de type acceptables. Cela permet d'écrire des fonctions génériques sans perte de sécurité de type.

Exemple de code :

package main import "fmt" type Adder[T any] func(a, b T) T func Sum[T any](slice []T, add Adder[T]) T { var result T for _, v := range slice { result = add(result, v) } return result } func intAdder(a, b int) int { return a + b } func main() { nums := []int{1, 2, 3, 4} sum := Sum(nums, intAdder) fmt.Println(sum) }

Caractéristiques clés :

  • Sécurité de type — exclusion d'erreurs de type au moment de la compilation.
  • Contraintes — possibilité de limiter les généricités uniquement à des types implémentant certaines interfaces ou possibilités de comparaison.
  • Compatibilité — les développeurs débutants peuvent utiliser les generics au fur et à mesure des besoins, sans avoir à restructurer les projets immédiatement.

Questions pièges.

Peut-on utiliser des opérations arithmétiques (+, -, *, /) pour n'importe quel paramètre de type T dans les generics ?

Non. Le compilateur Go ne sait pas si le type du paramètre supporte l'arithmétique. Pour cela, il faut spécifier une contrainte, par exemple, une interface avec des contraintes d'opérateurs à partir de Go 1.18+.

Exemple de code :

type Addable interface { int | float64 | uint } func Sum[T Addable](slice []T) T { var result T for _, v := range slice { result += v } return result }

Les conteneurs génériques peuvent-ils contenir des méthodes avec un comportement différent pour différents types ?

Non. Les méthodes des structures ou fonctions génériques sont identiques pour tous les types, sauf si l'on utilise un switch de type à l'intérieur de la méthode. Le comportement doit être défini par le biais de contraintes ou être purement paramétré.

Peut-on créer des types avec des paramètres de type (types génériques) au niveau du paquet, et pas seulement des fonctions ?

Oui, à partir de Go 1.18, il est possible de créer à la fois des fonctions génériques et des types de structures génériques :

type Stack[T any] struct { items []T } func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }

Erreurs typiques et anti-patterns

  • Utilisation de interface{} au lieu des generics dans les nouvelles versions de Go.
  • Oublier de spécifier des contraintes, ce qui entraîne l'impossibilité d'utiliser des opérations dans du code universel.
  • Généralisation excessive de fonctions simples (generics pour la généralisation).

Exemple de la vie réelle

Cas négatif

Dans une bibliothèque pour le travail avec des collections, une fonctionnalité Map universelle a été réalisée via interface{} :

Avantages :

  • Universel, peut être utilisé pour n'importe quel type.

Inconvénients :

  • Absence de sécurité de type, nécessité de conversions de types manuelles, erreurs à l'exécution.

Cas positif

Dans le même projet, ils sont passés aux generics et ont défini des contraintes via des interfaces :

Avantages :

  • Sécurité de type, les erreurs sont détectées pendant la compilation, facilitation de la maintenance.

Inconvénients :

  • Nécessite une connaissance de la nouvelle syntaxe, certains IDE ne prennent pas bien en charge les contraintes complexes.