Tout a commencé avec Go 1.18 : pourquoi les génériques ne sont pas qu'une simple fonctionnalité à la mode ?
\n\nPendant longtemps, la communauté Go s'est vantée de la simplicité du langage. L'absence de programmation générique (generics) était considérée comme un choix délibéré : « nous n'avons pas besoin de modèles complexes, nous écrivons du code compréhensible ». Mais la réalité a rattrapé son retard. Les développeurs en ont eu assez de copier-coller des fonctions pour int, string et float64 — ou d'utiliser interface{} avec un transtypage ultérieur, perdant ainsi la sécurité à la compilation.
La sortie de Go 1.18 (2022) a été une révolution. Les génériques (generics) ont ajouté au langage des paramètres de type (type parameters) et des contraintes (constraints). Désormais, les programmeurs Go peuvent écrire des structures de données et des algorithmes génériques sans sacrifier les performances et le typage strict.
\n\nDans cet article sur pythonlib.ru, nous allons comprendre comment fonctionnent les generics en Go, voir des exemples de code concrets et comprendre quand les utiliser, et quand ne pas le faire. C'est parti !
\n\nQue sont les generics en Go en termes simples ?
\n\nImaginez que vous écrivez une fonction qui trouve l'élément minimum dans une slice. Sans génériques, vous devriez créer trois versions : pour int, float64 et string. Ou utiliser interface{} avec un transtypage — lent et dangereux.
Les generics permettent d'écrire une seule fonction qui fonctionne avec n'importe quel type satisfaisant certaines conditions. La syntaxe est simple : après le nom de la fonction, entre crochets, on indique les paramètres de type.
\n\nSyntaxe : crochets et mot-clé any
\n\npackage main\n\nimport "fmt"\n\n// Min — fonction générique pour trouver le minimum\nfunc Min[T any](a, b T) T {\n // Erreur : l'opérateur < n'est pas défini pour any\n // if a < b {\n // return a\n // }\n return b\n}\n\nLe code ci-dessus ne compilera pas, car any est une interface vide (synonyme de interface{}). Le compilateur ne sait pas si le type supporte l'opérateur <. Il faut ajouter une contrainte (constraint).
Contraintes de types : constraints
\n\nEn Go, les contraintes sont définies via des interfaces. On peut utiliser celles intégrées du package golang.org/x/exp/constraints ou écrire les siennes.
import (\n "fmt"\n "golang.org/x/exp/constraints"\n)\n\n// Min fonctionne avec tout type ordonné\nfunc Min[T constraints.Ordered](a, b T) T {\n if a < b {\n return a\n }\n return b\n}\n\nfunc main() {\n fmt.Println(Min(10, 20)) // 10\n fmt.Println(Min(3.14, 2.71)) // 2.71\n fmt.Println(Min("apple", "banana")) // "apple"\n}\n\nNotez que constraints.Ordered inclut tous les types pour lesquels les opérateurs <, <=, >, >= sont définis — ce sont int, float, string.
Exemples pratiques : du simple au complexe
\n\nVoyons comment les generics en Go simplifient l'écriture de code réel.
\n\n1. Pile générique (Stack)
\n\nSans génériques, il faudrait stocker []interface{} et faire des assertions de type. Avec les generics, c'est élégant et sûr.
package main\n\nimport "fmt"\n\n// Stack — structure de données générique\ntype Stack[T any] struct {\n items []T\n}\n\nfunc (s *Stack[T]) Push(item T) {\n s.items = append(s.items, item)\n}\n\nfunc (s *Stack[T]) Pop() (T, bool) {\n if len(s.items) == 0 {\n var zero T\n return zero, false\n }\n item := s.items[len(s.items)-1]\n s.items = s.items[:len(s.items)-1]\n return item, true\n}\n\nfunc main() {\n intStack := Stack[int]{}\n intStack.Push(1)\n intStack.Push(2)\n val, ok := intStack.Pop()\n fmt.Println(val, ok) // 2 true\n\n strStack := Stack[string]{}\n strStack.Push("hello")\n