Introduction : que sont les Generics et pourquoi sont-ils apparus en Go ?
Pendant longtemps, la communauté Go (Golang) s'est passée de génériques (programmation générique). Les développeurs utilisaient des interfaces, des interface{} vides et de la génération de code pour contourner les limitations du typage strict. Dans la version Go 1.18 (février 2022), les génériques ont enfin été ajoutés au langage — un outil puissant permettant d'écrire des fonctions et des structures qui fonctionnent avec différents types de données sans perdre la sécurité des types.
Dans cet article, nous verrons pourquoi les Generics sont nécessaires en Go, comment ils fonctionnent, et nous apprendrons à les appliquer en pratique. Ce contenu est destiné aux développeurs débutants qui connaissent déjà les bases de Go : variables, fonctions et slices.
Pourquoi les génériques sont-ils nécessaires en Go ?
Imaginez que vous deviez écrire une fonction qui trouve la valeur minimale dans un slice de nombres. Sans génériques, vous devriez écrire des fonctions séparées pour int, float64 et d'autres types :
// Sans génériques — trois fonctions identiquesfunc MinInt(a, b int) int { if a < b { return a } return b}
func MinFloat64(a, b float64) float64 { if a < b { return a } return b}Cela entraîne une duplication de code. Auparavant, ce problème était résolu avec interface{} et le transtypage, mais cette approche perdait la sécurité au moment de la compilation :
func Min(a, b interface{}) interface{} { // Comparaison et transtypage — facile de se tromper return a}Les génériques résolvent trois problèmes clés :
- Sécurité des types — les erreurs de type sont détectées à la compilation.
- Élimination de la duplication — une seule fonction fonctionne avec n'importe quel type.
- Lisibilité du code — indication explicite des types pris en charge.
Bases de la syntaxe : paramètres de type et contraintes
Les génériques en Go reposent sur deux concepts : les paramètres de type (type parameters) et les contraintes (constraints).
Paramètres de type
Les paramètres de type sont indiqués entre crochets après le nom de la fonction ou du type. Par convention, on utilise le plus souvent la lettre T (pour Type), mais on peut utiliser n'importe quel nom :
// Fonction avec un paramètre de type Tfunc Print[T any](value T) { fmt.Println(value)}
// UtilisationPrint[int](42) // on spécifie explicitement le typePrint("Hello") // le compilateur déduit le type automatiquementLe mot-clé any est un alias pour l'interface vide interface{}. Il signifie que la fonction accepte n'importe quel type.
Contraintes (constraints)
Les contraintes définissent quels types peuvent être utilisés avec un générique. Par exemple, pour comparer des nombres, le type doit prendre en charge les opérateurs < et >. Les contraintes intégrées pour les types numériques se trouvent dans le package golang.org/x/exp/constraints :
import "golang.org/x/exp/constraints"
// Contrainte — uniquement les entiersfunc Sum[T constraints.Integer](numbers []T) T { var total T for _, n := range numbers { total += n } return total}Vous pouvez également créer vos propres contraintes via des interfaces :
// Contrainte personnalisée : le type doit avoir une méthode String()type Stringer interface { String() string}
func Describe[T Stringer](value T) string { return "Value: " + value.String()}Exemples pratiques d'utilisation des Generics
Examinons quelques scénarios réels où les génériques simplifient considérablement le code.
1. Fonction de recherche universelle dans un slice
// Trouve l'index d'un élément dans un slice. -1 si non trouvé.func Index[T comparable](slice []T, target T