Modern Go Engineering
Chapitre 10
10 - Go Generics
10 - Go Generics
Cours 10 : Go Generics
1. Introduction aux génériques
1.1 Pourquoi les génériques ?
Avant Go 1.18 (mars 2022), Go n'avait pas de génériques. Les développeurs utilisaient :
interface{}(any) avec type assertion- Code duplication pour chaque type
- Code generation (genny, stringer)
reflectpour la généricité au runtime
Problèmes sans génériques :
// Sans génériques : duplication
func SumInts(values []int) int {
var sum int
for _, v := range values {
sum += v
}
return sum
}
func SumFloats(values []float64) float64 {
var sum float64
for _, v := range values {
sum += v
}
return sum
}
// Sans génériques : interface{} + type assertion
func SumAny(values []any) any {
var sum float64
for _, v := range values {
switch val := v.(type) {
case int:
sum += float64(val)
case float64:
sum += val
}
}
return sum
}
Avec génériques :
func Sum[T ~int | ~float64](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}
1.2 Syntaxe de base
// Fonction générique avec un type parameter
func Min[T comparable](a, b T) T {
if a < b { // comparable ne permet pas <, il faut ordered
return a
}
return b
}
// Correction avec contrainte ordered
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
// Avec paramètres multiples
func Map[T, U any](input []T, fn func(T) U) []U {
result := make([]U, len(input))
for i, v := range input {
result[i] = fn(v)
}
return result
}
2. Type Parameters
2.1 Déclaration
Les type parameters sont déclarés entre crochets [] après le nom de la fonction/type :
// Syntaxe : func Nom[T Contrainte](params) resultats
func Identity[T any](value T) T {
return value
}
// Plusieurs paramètres
func Pair[T, U any](a T, b U) struct {
First T
Second U
} {
return struct {
First T
Second U
}{a, b}
}
// Avec variadic
func Concat[T any](slices ...[]T) []T {
var total int
for _, s := range slices {
total += len(s)
}
result := make([]T, 0, total)
for _, s := range slices {
result = append(result, s...)
}
return result
}
2.2 Type Inference
Go peut inférer les types dans la plupart des cas :
func main() {
// Inférence explicite
result := Min[int](3, 5)
// Inférence implicite (Go déduit le type)
result2 := Min(3, 5) // T = int
result3 := Min(3.14, 2.71) // T = float64
result4 := Min("a", "b") // T = string
// Map inference
doubled := Map([]int{1, 2, 3}, func(v int) int {
return v * 2
})
// L'inférence échoue si ambiguë
// Min(3, 3.14) // ERREUR: types différents
}
3. Contraintes intégrées
3.1 any
any est un alias pour interface{} — aucune contrainte :
func Print[T any](v T) {
fmt.Println(v) // fonctionne avec n'importe quel type
}
func IsNil[T any](v T) bool {
// Comparer avec nil nécessite un pointeur
// return v == nil // NE COMPILE PAS pour T = int
return any(v) == nil
}
3.2 comparable
comparable permet l'utilisation des opérateurs == et != :
func IndexOf[T comparable](slice []T, value T) int {
for i, v := range slice {
if v == value {
return i
}
}
return -1
}
func Contains[T comparable](slice []T, value T) bool {
return IndexOf(slice, value) >= 0
}
func Deduplicate[T comparable](slice []T) []T {
seen := make(map[T]struct{})
result := make([]T, 0, len(slice))
for _, v := range slice {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
result = append(result, v)
}
}
return result
}
func main() {
fmt.Println(IndexOf([]string{"a", "b", "c"}, "b")) // 1
fmt.Println(Contains([]int{1, 2, 3}, 4)) // false
fmt.Println(Deduplicate([]int{1, 1, 2, 3, 3})) // [1 2 3]
}
3.3 constraints.Ordered
golang.org/x/exp/constraints fournit Ordered qui supporte <, <=, >, >= :
package constraints
// Signed est un type entier signé
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Unsigned est un type entier non signé
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
// Integer est un type entier (signé ou non)
type Integer interface {
Signed | Unsigned
}
// Float est un type flottant
type Float interface {
~float32 | ~float64
}
// Ordered est un type ordonné (entier, flottant, string)
type Ordered interface {
Integer | Float | ~string
}
Utilisation :
import "golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func Clamp[T constraints.Ordered](value, min, max T) T {
if value < min {
return min
}
if value > max {
return max
}
return value
}
func Sort[T constraints.Ordered](slice []T) {
// Bubble sort pour l'exemple
n := len(slice)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if slice[j] > slice[j+1] {
slice[j], slice[j+1] = slice[j+1], slice[j]
}
}
}
}
4. Interfaces comme contraintes
4.1 Interface en tant que contrainte
// Interface classique comme contrainte
type Stringer interface {
String() string
}
func PrintToString[T Stringer](v T) {
fmt.Println(v.String())
}
// Interface avec méthodes et types
type Number interface {
~int | ~float64
IsPositive() bool
}
// Implémentation
type MyInt int
func (m MyInt) IsPositive() bool {
return m > 0
}
func ProcessNumber[T Number](v T) T {
if v.IsPositive() {
return v * 2
}
return v
}
4.2 Méthodes sur types génériques
// Interface avec méthodes génériques
type Container[T any] interface {
Get(index int) T
Set(index int, value T)
Len() int
}
type SliceContainer[T any] struct {
data []T
}
func (s SliceContainer[T]) Get(index int) T {
return s.data[index]
}
func (s SliceContainer[T]) Set(index int, value T) {
s.data[index] = value
}
func (s SliceContainer[T]) Len() int {
return len(s.data)
}
5. Type Sets et opérateur ~
5.1 Approximate type (~)
L'opérateur ~ permet d'accepter les types définis à partir d'un type sous-jacent :
// Sans ~ : seulement int exactement
func WithoutTilde[T int](v T) T {
return v
}
// Avec ~ : int et tous les types basés sur int
func WithTilde[T ~int](v T) T {
return v
}
type Age int
type Score int
func main() {
// WithoutTilde(Age(25)) // ERREUR: Age does not implement int
WithTilde(Age(25)) // OK: Age ~ int
WithTilde(Score(100)) // OK: Score ~ int
WithTilde(42) // OK: int ~ int
}
5.2 Type sets complexes
// Union de types
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
// Intersection (tous les types dans A ET B)
type ReadWriter interface {
io.Reader
io.Writer
}
// Types composites avec ~
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 |
~complex64 | ~complex128
}
func Sum[T Numeric](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}
6. Types génériques
6.1 Structs génériques
// Stack générique
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, error) {
if len(s.items) == 0 {
var zero T
return zero, fmt.Errorf("stack is empty")
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, nil
}
func (s *Stack[T]) Peek() (T, error) {
if len(s.items) == 0 {
var zero T
return zero, fmt.Errorf("stack is empty")
}
return s.items[len(s.items)-1], nil
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func (s *Stack[T]) IsEmpty() bool {
return len(s.items) == 0
}
// Binary Tree générique
type Tree[T any] struct {
Value T
Left *Tree[T]
Right *Tree[T]
}
func (t *Tree[T]) Insert(value T, less func(a, b T) bool) {
if less(value, t.Value) {
if t.Left == nil {
t.Left = &Tree[T]{Value: value}
} else {
t.Left.Insert(value, less)
}
} else {
if t.Right == nil {
t.Right = &Tree[T]{Value: value}
} else {
t.Right.Insert(value, less)
}
}
}
func (t *Tree[T]) InOrder() []T {
var result []T
if t.Left != nil {
result = append(result, t.Left.InOrder()...)
}
result = append(result, t.Value)
if t.Right != nil {
result = append(result, t.Right.InOrder()...)
}
return result
}
6.2 Maps génériques
// Map avec valeur par défaut
type DefaultMap[K comparable, V any] struct {
data map[K]V
default V
}
func NewDefaultMap[K comparable, V any](defaultValue V) *DefaultMap[K, V] {
return &DefaultMap[K, V]{
data: make(map[K]V),
default: defaultValue,
}
}
func (m *DefaultMap[K, V]) Get(key K) V {
if v, ok := m.data[key]; ok {
return v
}
return m.default
}
func (m *DefaultMap[K, V]) Set(key K, value V) {
m.data[key] = value
}
// Ordered Map (insertion order)
type OrderedMap[K comparable, V any] struct {
keys []K
values map[K]V
}
func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] {
return &OrderedMap[K, V]{
keys: make([]K, 0),
values: make(map[K]V),
}
}
func (m *OrderedMap[K, V]) Set(key K, value V) {
if _, ok := m.values[key]; !ok {
m.keys = append(m.keys, key)
}
m.values[key] = value
}
func (m *OrderedMap[K, V]) Get(key K) (V, bool) {
v, ok := m.values[key]
return v, ok
}
func (m *OrderedMap[K, V]) Iterate(fn func(K, V) bool) {
for _, key := range m.keys {
if !fn(key, m.values[key]) {
break
}
}
}
7. Méthodes génériques
7.1 Limitations importantes
Les méthodes ne peuvent PAS avoir de nouveaux type parameters :
type List[T any] struct {
items []T
}
// OK : T est déjà défini sur le type
func (l List[T]) Get(index int) T {
return l.items[index]
}
// ERREUR : les méthodes ne peuvent pas ajouter de type parameters
// func (l List[T]) Convert[U any](fn func(T) U) List[U] {
// // ...
// }
// Solution : fonction package-level
func ConvertList[T, U any](l List[T], fn func(T) U) List[U] {
result := make([]U, len(l.items))
for i, v := range l.items {
result[i] = fn(v)
}
return List[U]{items: result}
}
8. Patterns d'utilisation
8.1 Collections
// Set générique
type Set[T comparable] struct {
items map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{items: make(map[T]struct{})}
}
func (s *Set[T]) Add(item T) {
s.items[item] = struct{}{}
}
func (s *Set[T]) Remove(item T) {
delete(s.items, item)
}
func (s *Set[T]) Contains(item T) bool {
_, ok := s.items[item]
return ok
}
func (s *Set[T]) Union(other *Set[T]) *Set[T] {
result := NewSet[T]()
for item := range s.items {
result.Add(item)
}
for item := range other.items {
result.Add(item)
}
return result
}
func (s *Set[T]) Intersection(other *Set[T]) *Set[T] {
result := NewSet[T]()
for item := range s.items {
if other.Contains(item) {
result.Add(item)
}
}
return result
}
func (s *Set[T]) ToSlice() []T {
result := make([]T, 0, len(s.items))
for item := range s.items {
result = append(result, item)
}
return result
}
8.2 Functional patterns
// Map function
func Map[T, U any](input []T, fn func(T) U) []U {
result := make([]U, len(input))
for i, v := range input {
result[i] = fn(v)
}
return result
}
// Filter function
func Filter[T any](input []T, fn func(T) bool) []T {
result := make([]T, 0, len(input))
for _, v := range input {
if fn(v) {
result = append(result, v)
}
}
return result
}
// Reduce function
func Reduce[T, U any](input []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range input {
result = fn(result, v)
}
return result
}
// FlatMap function
func FlatMap[T, U any](input []T, fn func(T) []U) []U {
result := make([]U, 0, len(input))
for _, v := range input {
result = append(result, fn(v)...)
}
return result
}
// Usage
func main() {
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(v int) int {
return v * 2
}) // [2, 4, 6, 8, 10]
evens := Filter(numbers, func(v int) bool {
return v%2 == 0
}) // [2, 4]
sum := Reduce(numbers, 0, func(acc, v int) int {
return acc + v
}) // 15
pairs := FlatMap(numbers, func(v int) []int {
return []int{v, v * v}
}) // [1, 1, 2, 4, 3, 9, 4, 16, 5, 25]
}
8.3 Options pattern avec génériques
// Options pattern typé
type Option[T any] func(*T)
type ServerConfig struct {
Host string
Port int
Timeout time.Duration
TLS bool
MaxConn int
}
func WithHost[T *ServerConfig](host string) Option[T] {
return func(cfg T) {
cfg.Host = host
}
}
func WithPort[T *ServerConfig](port int) Option[T] {
return func(cfg T) {
cfg.Port = port
}
}
func WithTLS[T *ServerConfig](enabled bool) Option[T] {
return func(cfg T) {
cfg.TLS = enabled
}
}
func NewServerConfig[T *ServerConfig](opts ...Option[T]) T {
cfg := T(&ServerConfig{
Host: "localhost",
Port: 8080,
Timeout: 30 * time.Second,
})
for _, opt := range opts {
opt(cfg)
}
return cfg
}
// Builder pattern générique
type Builder[T any] struct {
value T
}
func NewBuilder[T any](initial T) *Builder[T] {
return &Builder[T]{value: initial}
}
func (b *Builder[T]) Set(fn func(*T)) *Builder[T] {
fn(&b.value)
return b
}
func (b *Builder[T]) Build() T {
return b.value
}
// Usage
config := NewBuilder(ServerConfig{}).
Set(func(c *ServerConfig) { c.Host = "example.com" }).
Set(func(c *ServerConfig) { c.Port = 443 }).
Set(func(c *ServerConfig) { c.TLS = true }).
Build()
9. Performance et Limitations
9.1 Performance impact
// Benchmark comparison
func BenchmarkGeneric(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sum(data)
}
}
func BenchmarkNonGeneric(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sum := 0
for _, v := range data {
sum += v
}
}
}
Les génériques en Go sont implémentés avec monomorphisation (stenciling) :
- Le compilateur génère une copie de la fonction pour chaque type utilisé
- Pas de boxing/unboxing comme en Java
- Performance équivalente au code manuel
- Augmentation de la taille du binaire
9.2 Limitations connues
// 1. Pas de type parameters sur les méthodes
type Wrapper[T any] struct {
Value T
}
// func (w Wrapper[T]) Convert[U any]() U {} // ERREUR
// 2. Pas de type parameters sur les closures
// fn := func[T any](v T) T { return v } // ERREUR
// 3. Pas de type assertion sur type parameter
func IsString[T any](v T) bool {
// _, ok := v.(string) // ERREUR
return false
}
// Solution : reflection
func IsStringReflect[T any](v T) bool {
_, ok := any(v).(string)
return ok
}
// 4. Pas d'opérateurs sur types génériques (sauf ==, != pour comparable)
// func Add[T any](a, b T) T { return a + b } // ERREUR
// 5. Pas de switch type sur type parameter sans interface
func Describe[T any](v T) {
// switch v.(type) { // ERREUR
// }
}
// 6. Contraintes cycliques interdites
// type A[T B[T]] interface {} // ERREUR
// 7. Pas de valeur nulle pour type parameter
func ReturnNil[T any]() *T {
return nil // OK: retourne un pointeur nil
// return T(nil) // ERREUR: seulement pour slices, maps, etc.
}
func Zero[T any]() T {
var zero T
return zero // OK: retourne la valeur zéro
}
9.3 Type inference limitations
// Go ne peut pas toujours inférer le type
func Convert[T, U any](input T, fn func(T) U) U {
return fn(input)
}
func main() {
// OK : inférence fonctionne
result := Convert(42, func(v int) string {
return fmt.Sprintf("%d", v)
})
// Problème : inférence ambiguë
// result2 := Convert(42, func(v int) int {
// return v
// })
// Solution : explicite
result2 := Convert[int, int](42, func(v int) int {
return v
})
}
10. Cas d'usage avancés
10.1 Repository pattern
type Repository[T any, ID comparable] interface {
FindByID(ctx context.Context, id ID) (T, error)
FindAll(ctx context.Context) ([]T, error)
Save(ctx context.Context, entity T) error
Delete(ctx context.Context, id ID) error
}
type InMemoryRepository[T any, ID comparable] struct {
data map[ID]T
idFn func(T) ID
mu sync.RWMutex
}
func NewInMemoryRepository[T any, ID comparable](idFn func(T) ID) *InMemoryRepository[T, ID] {
return &InMemoryRepository[T, ID]{
data: make(map[ID]T),
idFn: idFn,
}
}
func (r *InMemoryRepository[T, ID]) FindByID(ctx context.Context, id ID) (T, error) {
r.mu.RLock()
defer r.mu.RUnlock()
if entity, ok := r.data[id]; ok {
return entity, nil
}
var zero T
return zero, fmt.Errorf("not found")
}
func (r *InMemoryRepository[T, ID]) FindAll(ctx context.Context) ([]T, error) {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]T, 0, len(r.data))
for _, entity := range r.data {
result = append(result, entity)
}
return result, nil
}
func (r *InMemoryRepository[T, ID]) Save(ctx context.Context, entity T) error {
r.mu.Lock()
defer r.mu.Unlock()
r.data[r.idFn(entity)] = entity
return nil
}
func (r *InMemoryRepository[T, ID]) Delete(ctx context.Context, id ID) error {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.data, id)
return nil
}
// Usage
type User struct {
ID string
Name string
}
type UserRepository = *InMemoryRepository[User, string]
func NewUserRepository() UserRepository {
return NewInMemoryRepository(func(u User) string { return u.ID })
}
10.2 Pipeline pattern
type Pipeline[T any] struct {
stages []func(T) (T, error)
}
func NewPipeline[T any]() *Pipeline[T] {
return &Pipeline[T]{}
}
func (p *Pipeline[T]) Add(stage func(T) (T, error)) *Pipeline[T] {
p.stages = append(p.stages, stage)
return p
}
func (p *Pipeline[T]) Execute(input T) (T, error) {
current := input
for _, stage := range p.stages {
var err error
current, err = stage(current)
if err != nil {
return current, fmt.Errorf("pipeline failed: %w", err)
}
}
return current, nil
}
// Wrapper pour stages sync
func SyncStage[T any](fn func(T) T) func(T) (T, error) {
return func(v T) (T, error) {
return fn(v), nil
}
}
10.3 Result/Either pattern
type Result[T any] struct {
value T
err error
}
func Ok[T any](value T) Result[T] {
return Result[T]{value: value}
}
func Err[T any](err error) Result[T] {
return Result[T]{err: err}
}
func (r Result[T]) IsOk() bool {
return r.err == nil
}
func (r Result[T]) IsErr() bool {
return r.err != nil
}
func (r Result[T]) Unwrap() T {
if r.err != nil {
panic(r.err)
}
return r.value
}
func (r Result[T]) UnwrapOr(defaultValue T) T {
if r.err != nil {
return defaultValue
}
return r.value
}
func (r Result[T]) Map(fn func(T) T) Result[T] {
if r.err != nil {
return r
}
return Ok(fn(r.value))
}
func (r Result[T]) AndThen(fn func(T) Result[T]) Result[T] {
if r.err != nil {
return r
}
return fn(r.value)
}
11. Meilleures pratiques
11.1 Quand utiliser les génériques
✅ Bons usages :
- Fonctions utilitaires sur slices/maps (Map, Filter, Reduce)
- Structures de données (Stack, Queue, Set, Tree)
- Patterns de conception (Repository, Options, Builder)
- Algorithmes indépendants du type
❌ Mauvais usages :
- Quand
interface{}suffit - Pour des cas simples (un seul type)
- Quand ça réduit la lisibilité
- Pour remplacer l'héritage (Go n'en a pas)
11.2 Nommage
// Bon : une lettre pour les paramètres simples
func Map[T, U any](input []T, fn func(T) U) []U
// Bon : nom explicite pour contexte complexe
func Parse[Entity any](data []byte) (Entity, error)
// Bon : suffixe/prefixe cohérent
type SliceOf[T any] []T
type MapOf[K comparable, V any] map[K]V
12. Diagrammes
Hiérarchie des contraintes
Diagramme en cours de génération...
Pipeline générique
Diagramme en cours de génération...
Points Clés
- any =
interface{}, aucune contrainte - comparable = supporte
==et!= - ~ = accepte les types basés sur le type sous-jacent
- Monorphisation = performance équivalente au code manuel
- Pas de génériques sur les méthodes : utiliser des fonctions
- Lisibilité d'abord : les génériques doivent simplifier, pas complexifier