backend / go
Generics (Go 1.18+)
Explication
Ce que vous allez apprendre
- Écrire une fonction générique avec un paramètre de type
[T any] - Utiliser une contrainte de type précise plutôt que
anyquand c'est possible - Créer une contrainte custom avec une union de types (
~int | ~string) - Implémenter des fonctions d'ordre supérieur génériques :
Map,Filtrer,Reduce - Construire une struct générique réutilisable, comme une pile type-safe
Dans quel contexte ?
Avant Go 1.18 (sorti en 2022), un développeur qui voulait une fonction Max capable de comparer aussi bien des int que des float64 ou des string devait soit dupliquer la fonction pour chaque type, soit passer par any et perdre toute vérification de type à la compilation. Les generics résolvent ce dilemme vieux de dix ans dans l'histoire du langage.
D'abord, un paramètre de type ressemble à un paramètre de fonction, mais pour les types
func Max[T cmp.Ordered](a, b T) T déclare T comme un paramètre de type, contraint ici à cmp.Ordered (les types qui supportent <, >, etc.). Au moment de l'appel, Max(3, 7) instancie automatiquement T à int, sans jamais avoir à l'écrire explicitement — Go l'infère depuis les arguments fournis.
Cette approche donne à la fois la sécurité de type à la compilation (impossible de mélanger un int et une string dans le même appel) et la réutilisabilité d'une fonction unique pour tous les types compatibles.
Une fois cette syntaxe de base comprise, il faut savoir choisir sa contrainte
any (aucune contrainte) convient aux fonctions qui ne font rien de spécifique au type, comme Filtrer qui applique juste un prédicat. Une contrainte plus précise, comme cmp.Ordered ou une interface custom, est nécessaire dès que le corps de la fonction a besoin d'une opération spécifique (comparaison, addition...).
| Contrainte | Sens | Exemple d'usage |
|---|---|---|
any | Aucune contrainte, tout type accepté | Filtrer[T any], Map[T, U any] |
cmp.Ordered | Types comparables avec <, > | Max[T cmp.Ordered] |
~int ou ~string | Union de types (avec sous-types via ~) | Contrainte numérique custom |
Prérequis
Il faut être à l'aise avec les interfaces (leçon dédiée) : une contrainte de generics est en réalité une interface, avec une syntaxe étendue pour les unions de types.
Il reste une subtilité de syntaxe à bien comprendre : le tilde
~int dans une contrainte n'accepte pas seulement int, mais aussi tout type dont le type sous-jacent est int (par exemple type Age int). Sans le tilde, seul le type exact int serait accepté, ce qui exclurait injustement des types métier construits par-dessus un type numérique de base.
Piège fréquent
Généraliser prématurément une fonction avec des generics là où un type concret unique suffirait ajoute de la complexité sans bénéfice réel. Go reste un langage qui valorise la simplicité : n'introduis des generics que lorsque tu as réellement besoin de réutiliser une même logique sur plusieurs types différents.
Enfin, une struct peut aussi être générique
type Pile[T any] struct { elements []T } définit un conteneur type-safe réutilisable pour n'importe quel type — un cas d'usage où les generics apportent une vraie valeur, en remplaçant élégamment un ancien pattern basé sur any et des assertions de type partout.
Bonne pratique
Les fonctions d'ordre supérieur génériques (Map, Filtrer, Reduce) sont un excellent terrain d'entraînement pour comprendre les generics, mais évite d'en abuser dans du code métier Go idiomatique : une simple boucle for range reste souvent plus lisible qu'une chaîne de Map(Filtrer(...)).
Maintenant que tu maîtrises tout le cœur du langage, la suite du cours bascule vers des sujets très pratiques de développement backend, en commençant par la construction d'une véritable API REST avec net/http.
Commandes & code
Generics (Go 1.18+)
Écrire du code réutilisable sur plusieurs types sans sacrifier la sécurité de typage.
package main
import (
"cmp"
"fmt"
)
// Contrainte simple : Ordered depuis le package cmp (Go 1.21+), regroupe les types comparables avec < > etc.
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Fonction generique sur des slices : marche pour []int, []string, []float64...
func Filtrer[T any](s []T, predicat func(T) bool) []T {
resultat := make([]T, 0)
for _, v := range s {
if predicat(v) {
resultat = append(resultat, v)
}
}
return resultat
}
func Map[T, U any](s []T, f func(T) U) []U {
resultat := make([]U, len(s))
for i, v := range s {
resultat[i] = f(v)
}
return resultat
}
func Reduce[T, U any](s []T, initial U, f func(U, T) U) U {
acc := initial
for _, v := range s {
acc = f(acc, v)
}
return acc
}
// Contrainte custom : interface definissant les types acceptes
type Numerique interface {
~int | ~int64 | ~float64 // ~T accepte aussi les types dont le sous-jacent est T
}
func Somme[T Numerique](nombres []T) T {
var total T
for _, n := range nombres {
total += n
}
return total
}
// Struct generique : conteneur type-safe reutilisable
type Pile[T any] struct {
elements []T
}
func (p *Pile[T]) Empiler(v T) {
p.elements = append(p.elements, v)
}
func (p *Pile[T]) Depiler() (T, bool) {
var zero T
if len(p.elements) == 0 {
return zero, false
}
dernier := p.elements[len(p.elements)-1]
p.elements = p.elements[:len(p.elements)-1]
return dernier, true
}
func main() {
fmt.Println(Max(3, 7)) // instancie Max[int]
fmt.Println(Max("abc", "abd")) // instancie Max[string]
fmt.Println(Max(3.5, 2.1)) // instancie Max[float64]
nombres := []int{1, 2, 3, 4, 5, 6}
pairs := Filtrer(nombres, func(n int) bool { return n%2 == 0 })
fmt.Println(pairs)
carres := Map(nombres, func(n int) int { return n * n })
fmt.Println(carres)
texte := Map(nombres, func(n int) string { return fmt.Sprintf("n%d", n) })
fmt.Println(texte)
total := Reduce(nombres, 0, func(acc, n int) int { return acc + n })
fmt.Println(total)
fmt.Println(Somme([]int{1, 2, 3}))
fmt.Println(Somme([]float64{1.5, 2.5}))
pile := &Pile[string]{}
pile.Empiler("a")
pile.Empiler("b")
v, _ := pile.Depiler()
fmt.Println(v) // "b"
}Résumé
[T any]déclare un paramètre de type ;any= aucune contrainte, un type d'interface = contrainte.~int | ~string(union de types) permet des contraintes précises commeNumerique.- Generics brillent pour les conteneurs (
Pile[T]) et les fonctions d'ordre supérieur (Map,Filtrer,Reduce). - Ne pas généraliser prématurément : Go reste idiomatique avec des types concrets quand c'est suffisant.
Exercices pratiques
Mission : un type métier Quantite qui refuse d'utiliser la fonction Somme générique
Objectif : Comprendre le rôle du tilde dans une contrainte de generics, implémenter une fonction générique sûre, et raisonner sur la zero value dans un type paramétré.
Contexte
L'équipe stock définit type Quantite int pour éviter de confondre une quantité avec un simple entier ailleurs dans le code. Un développeur veut réutiliser la fonction générique Somme[T Numerique](nombres []T) T de la leçon pour additionner un []Quantite, et s'attend à devoir écrire une fonction dédiée SommeQuantites en pensant que Somme ne peut fonctionner qu'avec de "vrais" int.