Génériques du langage Go
Les génériques sont une fonctionnalité importante introduite dans Go version 1.18, permettant aux développeurs d’écrire du code plus flexible et réutilisable.
Les génériques sont principalement mis en œuvre à travers les deux concepts fondamentaux suivants :
- Paramètres de type :Permet d’utiliser un ou plusieurs types comme paramètres dans des définitions de fonctions ou de types.
- Contraintes de type :Spécifie les conditions que les paramètres de type doivent satisfaire, garantissant que ces types peuvent être manipulés en toute sécurité à l’intérieur des fonctions.
| Concepts | Fonction | Exemple |
|---|---|---|
| Paramètre de type | Déclaré après le nom de la fonction ou du type, représentant un type non spécifié. | [T any] |
| Contrainte de type | Définit les conditions que les paramètres de type doivent satisfaire (comme les opérateurs ou méthodes pris en charge). | int,float64,comparable,constraints.Ordered,any |
any |
Contraindre les paramètres de type au typeany. | [T any] |
comparable |
Contraindre les paramètres de type au typecomparable. | [K comparable] |
Les génériques nous permettent d’écrire du code qui ne dépend pas de types de données spécifiques.
Avant l'introduction des génériques, si nous voulions traiter différents types de données, nous devions généralement écrire des fonctions dupliquées pour chaque type.
Limites de l'approche traditionnelle :
Exemple
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
// Fonction pour gérer le type float64
func MaxFloat(a, b float64) float64 {
if a > b {
return a
}
return b
}
Solution utilisant les génériques :
Exemple
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
Explication détaillée de la syntaxe des génériques
Déclaration des paramètres de type
Les fonctions et types génériques sont déclarés au moyen de listes de paramètres de type, avec la syntaxe :[Type parameters Constraints]。
Exemple
funcNom de la fonction[Contrainte T](paramètre T)Type de la valeur de retour{
// Corps de la fonction
}
typenom du type[Contrainte T] struct {
// Champ de structure
}
Conventions de nommage des paramètres de type
- Habituellement, des lettres majuscules sont utilisées :
T、K、V、Ewait T: désigne le typeK: indique la cléV: indique la valeurE: désigne l'élément
Contraintes
Les contraintes définissent les conditions que les paramètres de type doivent satisfaire et constituent un concept fondamental des génériques.
Contraintes intégrées
1. anycontrainte
anyest une interface videinterface{}un alias de, ce qui signifie que tout type est autorisé.
Exemple
fmt.Printf("Value: %v, Type: %T\n", value, value)
}
// Exemple d'utilisation
PrintAny(42) // Valeur : 42, Type : int
PrintAny("hello") // Valeur : hello, Type : string
PrintAny(3.14) // Valeur : 3.14, Type : float64
2. comparablecontrainte
comparableIndique la prise en charge du type==et!=operator.
Exemple
for i, v := range slice {
if v == target {
return i
}
}
return -1
}
// Exemple d'utilisation
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(FindIndex(numbers, 3)) // Sortie : 2
names := []string{"Alice", "Bob", "Charlie"}
fmt.Println(FindIndex(names, "Bob")) // Sortie : 1
3. Contraintes d'union
Utilisation|L'opérateur combine plusieurs types.
Exemple
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Add[T Number](a, b T) T {
return a + b
}
// Exemple d'utilisation
fmt.Println(Add(10, 20)) // Sortie : 30
fmt.Println(Add(3.14, 2.71)) // Sortie : 5.85
Contrainte personnalisée
1. Contraintes de méthode
Définissez des contraintes qui exigent des méthodes spécifiques.
Exemple
type Stringer interface {
String() string
}
func PrintString[T Stringer](value T) {
fmt.Println(value.String())
}
// Implémenter un type personnalisé
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d ans)", p.Name, p.Âge)
}
// Exemple d'utilisation
person := Person{Name: "Alice", Age: 25}
PrintString(person) // Sortie : Alice (25 ans)
2. Contraintes complexes
Combinez les exigences de type et de méthode.
Exemple
type NumericStringer interface {
Number
String() string
}
Pratique des fonctions génériques
1. Fonctions utilitaires génériques
Exemple
func Swap[T any](a, b T) (T, T) {
return b, a
}
// Vérifier si la slice contient un élément
func Contains[T comparable](slice []T, target T) bool {
for _, item := range slice {
if item == target {
return true
}
}
return false
}
// Fonction de déduplication
func Unique[T comparable](slice []T) []T {
seen := make(map[T]bool)
result := []T{}
for _, item := range slice {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// Exemple d'utilisation
func main() {
// Exemple d'échange
a, b := 10, 20
a, b = Swap(a, b)
fmt.Printf("a=%d, b=%d\n", a, b) // Sortie : a=20, b=10
// Exemple de Contains
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(Contains(numbers, 3)) // Sortie : true
// Exemple de Unique
duplicates := []int{1, 2, 2, 3, 4, 4, 5}
unique := Unique(duplicates)
fmt.Println(unique) // Sortie : [1 2 3 4 5]
}
2. Fonctions d'opérations mathématiques
Exemple
func Max[T Number](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
max := slice[0]
for _, value := range slice[1:] {
if value > max {
max = value
}
}
return max
}
// Trouver la valeur minimale d'une slice
func Min[T Number](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
min := slice[0]
for _, value := range slice[1:] {
if value < min {
min = value
}
}
return min
}
// Trouver la valeur moyenne d'une tranche
func Average[T Number](slice []T) float64 {
if len(slice) == 0 {
return 0
}
var sum T
for _, value := range slice {
sum += value
}
return float64(sum) / float64(len(slice))
}
// Exemple d'utilisation
func main() {
ints := []int{1, 5, 3, 9, 2}
floats := []float64{1.1, 5.5, 3.3, 9.9, 2.2}
fmt.Printf("Max int: %d\n", Max(ints)) // Sortie : 9
fmt.Printf("Min float: %.1f\n", Min(floats)) // Sortie : 1.1
fmt.Printf("Moyenne : %.2f\n", Moyenne(floats)) // Sortie : 4.40
}
Types génériques
1. Structures génériques
Exemple
type Stack[T any] struct {
éléments []T
}
// Empiler
func (s *Stack[T]) Push(value T) {
s.elements = append(s.elements, value)
}
// Dépiler
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
lastIndex := len(s.elements) - 1
value := s.elements[lastIndex]
s.elements = s.elements[:lastIndex]
return value, true
}
// Consulter l'élément au sommet
func (s *Stack[T]) Peek() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
return s.elements[len(s.elements)-1], true
}
// Vérifier si la pile est vide
func (s *Stack[T]) IsEmpty() bool {
return len(s.elements) == 0
}
// Exemple d'utilisation
func main() {
// Pile d'entiers
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
fmt.Println(intStack.Pop()) // Sortie : 3 true
// Pile de chaînes
stringStack := Stack[string]{}
stringStack.Push("hello")
stringStack.Push("world")
fmt.Println(stringStack.Pop()) // Sortie : world true
}
2. Table générique (Map)
Exemple
type SafeMap[K comparable, V any] struct {
data map[K]V
mutex sync.RWMutex
}
// Créer une nouvelle SafeMap
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
return &SafeMap[K, V]{
data: make(map[K]V),
}
}
// Définir la paire clé-valeur
func (m *SafeMap[K, V]) Set(key K, value V) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data[key] = value
}
// Obtenir la valeur
func (m *SafeMap[K, V]) Get(key K) (V, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
value, exists := m.data[key]
return value, exists
}
// Supprimer la clé
func (m *SafeMap[K, V]) Delete(key K) {
m.mutex.Lock()
defer m.mutex.Unlock()
delete(m.data, key)
}
// Obtenir toutes les clés
func (m *SafeMap[K, V]) Keys() []K {
m.mutex.RLock()
defer m.mutex.RUnlock()
keys := make([]K, 0, len(m.data))
for key := range m.data {
keys = append(keys, key)
}
return keys
}
// Exemple d'utilisation
func main() {
// Créer une map de chaînes vers entiers
scores := NewSafeMap[string, int]()
scores.Set("Alice", 95)
scores.Set("Bob", 87)
if score, exists := scores.Get("Alice"); il existe {
fmt.Printf("Score d'Alice : %d"\n", score) // Sortie : Score d'Alice : 95
}
fmt.Println("Clés :", scores.Keys()) // Sortie : Clés : [Alice Bob]
}
Inférence de type
Le compilateur Go peut inférer automatiquement les paramètres de type, ce qui rend le code plus concis.
Exemple
func main() {
// Exemple d'inférence de type
fmt.Println(Max([]int{1, 2, 3})) // Le compilateur infère T comme int
fmt.Println(Max([]float64{1.1, 2.2})) // Le compilateur infère T comme float64
// Spécifier explicitement le type (parfois nécessaire)
var result int = Max[int]([]int{1, 2, 3})
fmt.Println(result)
}
Exercices pratiques
Exercice 1 : Implémenter un filtre générique
Écrivez uneFilterfonction qui filtre les éléments d'une slice selon une condition.
Exemple
func Filter[T any](slice []T, predicate func(T) bool) []T {
// Implémenter la logique de filtrage
}
// Code de test
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
even := Filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(even) // Devrait afficher : [2 4 6]
}
Exercice 2 : Implémenter une fonction map générique
Écrivez uneMapUne fonction qui convertit chaque élément d'une slice en un autre type.
Exemple
func Map[T any, U any](slice []T, mapper func(T) U) []U {
// Implémenter la logique de mappage
}
// Code de test
func main() {
numbers := []int{1, 2, 3, 4, 5}
strings := Map(numbers, func(n int) string {
return fmt.Sprintf("Number: %d", n)
})
fmt.Println(strings)
}
Problèmes courants et considérations
1. Considérations sur les performances
Les génériques effectuent une spécialisation de type à la compilation, avec des performances d'exécution comparables à celles d'un code spécifique écrit à la main.
2. Choix des contraintes de type
- Utilisation
anyest le plus flexible lorsque, mais limité en fonctionnalités. - Utilisation
comparablePrend en charge la comparaison d'égalité - Utilisez des contraintes d'union pour restreindre les types concrets disponibles.
3. Gestion des erreurs
Exemple
func SafeMax[T Number](slice []T) (T, error) {
if len(slice) == 0 {
var zero T
return zero, errors.New("slice is empty")
}
return Max(slice), nil
}