MFormations
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)
  • reflect pour 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

  1. any = interface{}, aucune contrainte
  2. comparable = supporte == et !=
  3. ~ = accepte les types basés sur le type sous-jacent
  4. Monorphisation = performance équivalente au code manuel
  5. Pas de génériques sur les méthodes : utiliser des fonctions
  6. Lisibilité d'abord : les génériques doivent simplifier, pas complexifier