Le problème : écrire deux fois le même code
Vous voulez une fonction qui renvoie le plus grand de deux valeurs. Pour des int, facile. Mais pour des float64, il faut la réécrire à l'identique, juste avec un autre type :
func MaxInt(a, b int) int {
if a > b { return a }
return b
}
func MaxFloat(a, b float64) float64 { // exactement le même code...
if a > b { return a }
return b
}
Avant Go 1.18, on n'avait que deux mauvaises options : dupliquer (une fonction par type), ou passer par interface{} et perdre toute la sécurité de typage (et payer des conversions). Les génériques règlent ça.
Les paramètres de type [T]
Un paramètre de type se déclare entre crochets, juste après le nom de la fonction. T est un type « à trous » que le compilateur remplit à l'appel :
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(3, 5)) // 5 → T vaut int
fmt.Println(Max(2.5, 1.0)) // 2.5 → T vaut float64
}
[T int | float64] dit : « T peut être un int ou un float64 ». Une seule fonction, et le compilateur génère la bonne version pour chaque type d'appel, sans perdre le typage.
Depuis Go 1.21, max et min existent comme fonctions intégrées : pas besoin d'écrire Max en vrai. L'exemple sert à montrer le mécanisme, qui s'applique à tout ce que vous écrirez de générique.
Les contraintes : ce que T a le droit d'être
Le bout entre crochets s'appelle une contrainte : elle limite les types autorisés ET détermine les opérations permises sur T. Trois cas à connaître :
any: n'importe quel type (c'est l'alias deinterface{}). Mais alors on ne peut presque rien faire surTà part le stocker.comparable: les types qu'on peut comparer avec==et!=(utile pour les clés de map).- une contrainte sur mesure : une interface qui énumère des types.
On veut écrire une fonction générique Max[T any](a, b T) T qui renvoie le plus grand des deux. Elle contient if a > b { return a }. Avant de dérouler : cette fonction compile-t-elle ? Le a > b est-il accepté quand T est any ? Que faut-il changer pour que ça marche ?
Voir la réponse
Non, elle ne compile pas. La contrainte any autorise n'importe quel type, y compris des types qui ne savent pas se comparer avec > (structs, maps, slices, bool...). Le compilateur refuse donc a > b, car l'opération n'est pas garantie pour tout T. La solution : contraindre T aux types ordonnés avec cmp.Ordered (Go 1.21) : func Max[T cmp.Ordered](a, b T) T. Cette contrainte regroupe les entiers, flottants et chaînes, soit tout ce qui supporte < <= >= >. Une contrainte est exactement ça : la promesse de ce que T sait faire ; elle débloque dans le corps de la fonction les opérations qu'elle garantit.
// ~int signifie "int et tout type dont le sous-jacent est int"
type Nombre interface {
~int | ~int64 | ~float64
}
func Somme[T Nombre](valeurs []T) T {
var total T
for _, v := range valeurs {
total += v // autorisé : la contrainte garantit le +
}
return total
}
Pour les types ordonnés, la bibliothèque standard fournit déjà cmp.Ordered (Go 1.21) : func Max[T cmp.Ordered](a, b T) T couvre d'un coup les nombres ET les string.
Les types génériques
Pas que les fonctions : un type aussi peut être paramétré. L'exemple classique, une pile (stack) qui marche pour n'importe quel type :
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() {
var p Pile[string] // une pile de strings, typée
p.Empiler("a")
p.Empiler("b")
v, ok := p.Depiler()
fmt.Println(v, ok) // b true
}
La même Pile sert pour Pile[int], Pile[Compte], etc. : un seul code, typé et vérifié à la compilation pour chaque usage.
Quand (et quand ne pas) les utiliser
Les génériques brillent pour les conteneurs (piles, listes, caches) et les algorithmes qui traitent plein de types de la même façon (Map, Filter, Somme). Mais ils ne remplacent pas les interfaces.
Génériques ou interface ? Si tu as besoin d'un comportement commun (des types qui savent faire Aire()), c'est une interface. Si tu fais la même opération sur des types différents en gardant leur type exact (une pile de T), c'est un générique. Dans le doute, commence simple : n'introduis un générique que quand la duplication devient réelle.
À toi : complète cette fonction générique Contient qui dit si une valeur est dans un slice, puis exécute :
package main
import "fmt"
// Contient renvoie true si v est présent dans s.
func Contient[T comparable](s []T, v T) bool {
for _, e := range s {
if e == v { // == est permis grâce à comparable
return true
}
}
return false
}
func main() {
fmt.Println(Contient([]int{1, 2, 3}, 2)) // true
fmt.Println(Contient([]string{"a", "b"}, "z")) // false
}
À remarquer. La contrainte comparable est ce qui autorise le == dans la fonction. Avec any à la place, le code ne compilerait pas : Go saurait stocker v mais pas la comparer. La contrainte ne limite pas seulement les types, elle débloque les opérations.
The problem: writing the same code twice
You want a function that returns the larger of two values. For int, easy. But for float64, you have to rewrite it identically, just with another type:
func MaxInt(a, b int) int {
if a > b { return a }
return b
}
func MaxFloat(a, b float64) float64 { // the exact same code...
if a > b { return a }
return b
}
Before Go 1.18, you only had two bad options: duplicate (one function per type), or go through interface{} and lose all type safety (and pay for conversions). Generics fix this.
Type parameters [T]
A type parameter is declared in brackets, right after the function name. T is a "fill-in-the-blank" type the compiler fills at the call site:
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(3, 5)) // 5 → T is int
fmt.Println(Max(2.5, 1.0)) // 2.5 → T is float64
}
[T int | float64] says: "T can be an int or a float64". A single function, and the compiler generates the right version for each call type — without losing typing.
Since Go 1.21, max and min exist as built-in functions: you don't actually need to write Max. The example just shows the mechanism, which applies to anything generic you write.
Constraints: what T is allowed to be
The bit in brackets is called a constraint: it limits the allowed types AND determines which operations are permitted on T. Three cases to know:
any: any type at all (it's the alias forinterface{}). But then you can barely do anything withTbeyond storing it.comparable: types you can compare with==and!=(useful for map keys).- a custom constraint: an interface that lists types.
We want to write a generic function Max[T any](a, b T) T that returns the larger of two values. It contains if a > b { return a }. Before scrolling: does this function compile? Is a > b accepted when T is any? What needs to change for it to work?
See the answer
No, it does not compile. The any constraint allows any type, including types that cannot be compared with > (structs, maps, slices, bool...). The compiler therefore refuses a > b, because the operation is not guaranteed for every T. The fix: constrain T to ordered types using cmp.Ordered (Go 1.21): func Max[T cmp.Ordered](a, b T) T. That constraint covers integers, floats and strings, i.e. everything that supports < <= >= >. A constraint is exactly that: a promise of what T can do; it unlocks inside the function body the operations the constraint guarantees.
// ~int means "int and any type whose underlying type is int"
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](values []T) T {
var total T
for _, v := range values {
total += v // allowed: the constraint guarantees +
}
return total
}
For ordered types, the standard library already provides cmp.Ordered (Go 1.21): func Max[T cmp.Ordered](a, b T) T covers numbers AND strings in one go.
Generic types
Not just functions: a type can be parameterized too. The classic example, a stack that works for any type:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
func main() {
var s Stack[string] // a stack of strings, typed
s.Push("a")
s.Push("b")
v, ok := s.Pop()
fmt.Println(v, ok) // b true
}
The same Stack serves Stack[int], Stack[Account], etc.: one source, typed and checked at compile time for each use.
When (and when not) to use them
Generics shine for containers (stacks, lists, caches) and algorithms that handle many types the same way (Map, Filter, Sum). But they don't replace interfaces.
Generics or interface? If you need shared behavior (types that can do Area()), that's an interface. If you do the same operation on different types while keeping their exact type (a stack of T), that's a generic. When in doubt, start simple: only introduce a generic when duplication becomes real.
Your turn: complete this generic Contains function that says whether a value is in a slice, then run it:
package main
import "fmt"
// Contains reports whether v is in s.
func Contains[T comparable](s []T, v T) bool {
for _, e := range s {
if e == v { // == is allowed thanks to comparable
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Contains([]string{"a", "b"}, "z")) // false
}
Notice. The comparable constraint is what allows == inside the function. With any instead, the code wouldn't compile: Go would know how to store v but not compare it. A constraint doesn't only limit types, it unlocks operations.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
✨ Prompt IA
Copiez ce prompt dans Claude ou ChatGPT :
Écris en Go une fonction générique Map[T, U any](s []T, f func(T) U) []U qui transforme un slice. Montre-la utilisée pour doubler des entiers puis pour récupérer la longueur de chaînes. Explique la contrainte choisie.
💬 Ré-explique sans regarder
L'IA te rend une fonction Somme[T Nombre]. Explique avec tes mots : à quoi sert la contrainte Nombre, et pourquoi on ne peut pas juste écrire [T any] ici ?
Nombre limite T aux types numériques (~int | ~float64…) ET, surtout, elle autorise l'opération + sur T. Avec [T any], T pourrait être n'importe quoi (un bool, un struct) : le compilateur refuserait total += v car l'addition n'a pas de sens pour tous les types. La contrainte ne fait pas que filtrer les types, elle débloque ce qu'on a le droit de faire avec.⚖️ Juge le code de l'IA
L'IA propose cette fonction générique pour additionner un slice. L'accepter ou la rejeter, et pourquoi ?
func Somme[T any](valeurs []T) T {
var total T
for _, v := range valeurs {
total += v
}
return total
}
any autorise n'importe quel type, donc Go interdit total += v : l'addition n'a pas de sens pour un bool ou un struct. Il faut une contrainte qui garantit le +, par exemple [T ~int | ~float64] (ou un type Nombre dédié). Règle : la contrainte doit autoriser toutes les opérations utilisées dans le corps.🧠 Rappel libre
Sans remonter : quelle contrainte faut-il pour pouvoir comparer deux T avec == dans une fonction générique ?
comparable : func F[T comparable](…). Elle autorise == et != sur T (et c'est aussi ce qu'exigent les clés de map). Avec any, le compilateur refuserait e == v, car certains types (slices, maps, fonctions) ne sont pas comparables.Vos types ont du comportement (méthodes, interfaces) et votre code marche pour plusieurs types (génériques). On passe au superpouvoir de Go : faire tourner du code en parallèle avec les goroutines et les channels.
Leçon 8 : Goroutines et channels →