Introduction : que sont les Generics et pourquoi sont-ils apparus dans Go
Pendant longtemps, la communauté Go s'est passée de generics (types génériques). Les développeurs utilisaient interface{} et le transtypage manuel, ce qui conduisait à un code lourd et à des erreurs lors de l'exécution. Avec la sortie de la version Go 1.18 (février 2022), les generics ont été officiellement ajoutés au langage — l'une des fonctionnalités les plus attendues.
Les Generics (ou polymorphisme paramétrique) permettent d'écrire des fonctions et des structures de données qui fonctionnent avec n'importe quel type, tout en conservant un typage strict lors de la compilation. Cela signifie que vous pouvez écrire un seul algorithme (par exemple, un tri ou une recherche) et l'utiliser pour int, string, des structures personnalisées — sans duplication de code et sans perte de sécurité des types.
Dans cet article, nous verrons pourquoi les generics sont nécessaires en Go, comment les utiliser correctement et quels pièges il faut prendre en compte.
Partie principale : comment fonctionnent les Generics en Go
1. Syntaxe des paramètres de type
La syntaxe de base des generics en Go est la suivante : après le nom de la fonction ou du type, entre crochets, on indique les paramètres de type. Examinons l'exemple le plus simple — une fonction qui retourne la valeur passée (analogue de identity) :
package main
import "fmt"
// Fonction générique : T est un paramètre de typefunc Print[T any](value T) { fmt.Println(value)}
func main() { Print[int](42) // indication explicite du type Print[string]("Go") // indication explicite du type Print("Go 1.18") // inférence de type implicite (type inference)}Le mot-clé any est un alias pour interface{}. Il signifie que la fonction accepte une valeur de n'importe quel type. Cependant, la véritable puissance des generics se révèle avec l'utilisation de contraintes (constraints).
2. Contraintes (constraints) et interfaces
Les contraintes permettent d'indiquer quelles opérations peuvent être effectuées avec le paramètre de type. Par exemple, pour additionner deux nombres, il faut garantir que le type prend en charge les opérateurs +, -, *, etc. En Go, on utilise pour cela des interfaces spéciales du package golang.org/x/exp/constraints ou des interfaces personnalisées :
package main
import ( "fmt" "golang.org/x/exp/constraints")
// Contrainte : T doit prendre en charge la comparaisonfunc Max[T constraints.Ordered](a, b T) T { if a > b { return a } return b}
func main() { fmt.Println(Max(10, 20)) // 20 fmt.Println(Max("apple", "banana")) // banana // fmt.Println(Max([]int{1}, []int{2})) // Erreur de compilation : les slices ne sont pas ordonnés}L'interface intégrée constraints.Ordered inclut les types qui prennent en charge les opérateurs <, <=, >, >= : tous les types numériques et string.
Vous pouvez également créer vos propres contraintes :
// Contrainte personnalisée : le type doit avoir une méthode String()type Stringer interface { String() string}
func Describe[T Stringer](v T) string { return "Value: " + v.String()}3. Structures et méthodes génériques
Les generics en Go fonctionnent non seulement avec les fonctions, mais aussi avec les types personnalisés. Examinons un exemple classique — une pile générique :
package main
import "fmt"
// Structure générique Stacktype Stack[T any] struct { items []T}
// Push ajoute un élément à la pilefunc (s *Stack[T]) Push(item T) { s.items = append(s.items, item)}
// Pop retire un élément de la pilefunc (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}
func main() {