Go Generics en 2026 : Paramètres de Type, Contraintes et Questions d'Entretien

Guide complet sur les génériques en Go : paramètres de type, interfaces de contraintes, patterns avancés et questions techniques fréquentes en entretien d'embauche pour développeurs Go.

Go Generics en 2026 : Paramètres de Type, Contraintes et Questions d'Entretien

Les génériques représentent l'une des évolutions les plus significatives du langage Go depuis sa création. Introduits avec Go 1.18 en 2022, les paramètres de type permettent désormais d'écrire du code réutilisable sans sacrifier la sécurité de typage statique. En 2026, les génériques sont devenus un outil incontournable pour tout développeur Go, et leur maîtrise constitue un critère d'évaluation majeur lors des entretiens techniques. Cet article explore en profondeur les paramètres de type, les contraintes d'interface, les patterns avancés et les questions d'entretien les plus fréquentes.

Les génériques résolvent le problème de la duplication de code

Avant Go 1.18, les développeurs devaient soit dupliquer le code pour chaque type, soit utiliser interface avec des assertions de type coûteuses en performance et sujettes aux erreurs à l'exécution. Les génériques permettent d'écrire une seule implémentation qui fonctionne avec n'importe quel type satisfaisant les contraintes spécifiées, tout en conservant la vérification de type à la compilation.

Syntaxe des Paramètres de Type

La syntaxe des génériques en Go utilise des crochets pour déclarer les paramètres de type. Un paramètre de type est défini avec un nom suivi d'une contrainte qui spécifie les opérations autorisées sur ce type.

go
package main

import "fmt"

// Generic function with type parameter T
func Print[T any](value T) {
    fmt.Println(value)
}

// Generic function with multiple type parameters
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func main() {
    Print(42)
    Print("hello")
    Print(3.14)

    numbers := []int{1, 2, 3, 4, 5}
    doubled := Map(numbers, func(n int) int { return n * 2 })
    fmt.Println(doubled) // [2 4 6 8 10]

    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("num:%d", n)
    })
    fmt.Println(strings) // [num:1 num:2 num:3 num:4 num:5]
}

Le mot-clé any est un alias pour interface{} et représente la contrainte la plus permissive. Lorsqu'un paramètre de type utilise any, aucune opération spécifique n'est garantie sur ce type, hormis l'assignation et le passage en paramètre.

Contraintes d'Interface et Types Prédéfinis

Les contraintes permettent de restreindre les types acceptés par une fonction ou une structure générique. Go 1.18 a introduit le package constraints (déplacé vers cmp et slices dans Go 1.21+) ainsi que la possibilité de définir des contraintes personnalisées.

go
package main

import (
    "cmp"
    "fmt"
)

// Custom constraint using type union
type Number interface {
    int | int32 | int64 | float32 | float64
}

// Function constrained to Number types
func Sum[T Number](numbers []T) T {
    var total T
    for _, n := range numbers {
        total += n
    }
    return total
}

// Using cmp.Ordered for comparable types
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Constraint with method requirement
type Stringer interface {
    String() string
}

func PrintAll[T Stringer](items []T) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    fmt.Println(Sum(ints)) // 15

    floats := []float64{1.5, 2.5, 3.0}
    fmt.Println(Sum(floats)) // 7.0

    fmt.Println(Max(10, 20))       // 20
    fmt.Println(Max("abc", "xyz")) // xyz
}

L'opérateur | permet de créer des unions de types dans une contrainte. L'opérateur ~ (tilde) permet d'inclure tous les types dont le type sous-jacent correspond, ce qui est essentiel pour les types personnalisés basés sur des types primitifs.

Structures et Méthodes Génériques

Les structures peuvent également utiliser des paramètres de type, permettant de créer des conteneurs et des structures de données réutilisables.

go
package main

import "fmt"

// Generic Stack implementation
type Stack[T any] struct {
    items []T
}

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{items: make([]T, 0)}
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func (s *Stack[T]) Peek() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    return s.items[len(s.items)-1], true
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

// Generic Pair type
type Pair[K, V any] struct {
    Key   K
    Value V
}

func main() {
    // Stack of integers
    intStack := NewStack[int]()
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)

    for intStack.Len() > 0 {
        val, _ := intStack.Pop()
        fmt.Println(val) // 3, 2, 1
    }

    // Stack of strings
    strStack := NewStack[string]()
    strStack.Push("hello")
    strStack.Push("world")

    // Pair usage
    pair := Pair[string, int]{Key: "age", Value: 30}
    fmt.Printf("%s: %d\n", pair.Key, pair.Value)
}

Les méthodes génériques ne peuvent pas introduire de nouveaux paramètres de type; elles utilisent les paramètres déclarés au niveau de la structure.

Inférence de Type et Instanciation

Le compilateur Go peut souvent inférer les paramètres de type à partir des arguments fournis, simplifiant l'appel des fonctions génériques.

go
package main

import "fmt"

func First[T any](slice []T) (T, bool) {
    if len(slice) == 0 {
        var zero T
        return zero, false
    }
    return slice[0], true
}

func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

func main() {
    // Type inference - no need to specify [int]
    nums := []int{1, 2, 3}
    first, ok := First(nums)
    fmt.Println(first, ok) // 1 true

    // Explicit type parameters when needed
    empty, ok := First[string]([]string{})
    fmt.Println(empty, ok) // "" false

    // Map with inferred types
    ages := map[string]int{"alice": 30, "bob": 25}
    names := Keys(ages)
    fmt.Println(names) // [alice bob] or [bob alice]
}

L'inférence de type fonctionne de gauche à droite et ne peut pas inférer des types qui n'apparaissent pas dans les paramètres de la fonction.

Patterns Avancés : Interfaces Génériques et Composition

Les interfaces peuvent être paramétrées, permettant de définir des contrats génériques réutilisables.

go
package main

import "fmt"

// Generic Repository interface
type Repository[T any, ID comparable] interface {
    FindByID(id ID) (T, error)
    Save(entity T) error
    Delete(id ID) error
    FindAll() ([]T, error)
}

// Generic Result type for error handling
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]) Unwrap() (T, error) {
    return r.value, r.err
}

func (r Result[T]) Map(fn func(T) T) Result[T] {
    if r.err != nil {
        return r
    }
    return Ok(fn(r.value))
}

// Generic Option type
type Option[T any] struct {
    value *T
}

func Some[T any](value T) Option[T] {
    return Option[T]{value: &value}
}

func None[T any]() Option[T] {
    return Option[T]{value: nil}
}

func (o Option[T]) IsSome() bool {
    return o.value != nil
}

func (o Option[T]) Unwrap() T {
    if o.value == nil {
        panic("called Unwrap on None")
    }
    return *o.value
}

func (o Option[T]) UnwrapOr(defaultValue T) T {
    if o.value == nil {
        return defaultValue
    }
    return *o.value
}

func main() {
    result := Ok(42).Map(func(n int) int { return n * 2 })
    val, err := result.Unwrap()
    fmt.Println(val, err) // 84 <nil>

    opt := Some("hello")
    fmt.Println(opt.IsSome())      // true
    fmt.Println(opt.Unwrap())      // hello

    none := None[int]()
    fmt.Println(none.UnwrapOr(0))  // 0
}

Questions d'Entretien Fréquentes sur les Génériques Go

Les entretiens techniques en Go incluent de plus en plus de questions sur les génériques. Voici les questions les plus courantes avec leurs réponses attendues.

Question 1 : Quelle est la différence entre any et comparable ?

any est un alias pour interface{} et accepte tout type. comparable est une contrainte prédéfinie qui n'accepte que les types supportant les opérateurs == et !=. Les slices, maps et fonctions ne sont pas comparables et ne satisfont pas comparable.

Question 2 : Pourquoi ne peut-on pas utiliser d'opérateurs arithmétiques avec any ?

Les opérateurs arithmétiques (+, -, *, /) ne sont pas définis pour tous les types. Il faut utiliser une contrainte comme Number ou cmp.Ordered pour garantir que le type supporte ces opérations.

go
// This won't compile - any doesn't support +
func AddWrong[T any](a, b T) T {
    return a + b // compile error
}

// Correct approach with constraint
type Numeric interface {
    ~int | ~int64 | ~float64
}

func Add[T Numeric](a, b T) T {
    return a + b // works
}

Question 3 : Comment implémenter une fonction générique Filter ?

go
func Filter[T any](slice []T, predicate func(T) bool) []T {
    result := make([]T, 0)
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

// Usage
numbers := []int{1, 2, 3, 4, 5, 6}
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
// evens = [2, 4, 6]

Question 4 : Quelle est la différence entre ~int et int dans une contrainte ?

L'opérateur tilde (~) inclut tous les types dont le type sous-jacent est int, y compris les types définis comme type MyInt int. Sans tilde, seul le type exact int est accepté.

go
type MyInt int

// Only accepts int, not MyInt
type StrictInt interface {
    int
}

// Accepts int and any type with underlying type int (like MyInt)
type FlexibleInt interface {
    ~int
}

Prêt à réussir tes entretiens Go ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Limitations et Bonnes Pratiques

Les génériques en Go ont certaines limitations intentionnelles pour préserver la simplicité du langage. Les méthodes ne peuvent pas avoir leurs propres paramètres de type. La spécialisation de type (fournir une implémentation différente pour un type spécifique) n'est pas supportée. Les types récursifs avec paramètres de type ont des restrictions.

Les bonnes pratiques recommandent d'utiliser les génériques lorsqu'ils simplifient réellement le code, pas simplement parce qu'ils sont disponibles. Si une fonction ne fonctionne qu'avec deux ou trois types concrets, des fonctions séparées peuvent être plus claires. Les génériques sont particulièrement utiles pour les structures de données, les algorithmes de collection et les patterns comme Result/Option.

Conclusion

Les génériques transforment la façon dont le code Go est écrit et organisé. Ils permettent de créer des bibliothèques et des abstractions réutilisables tout en conservant la sécurité de type qui fait la force de Go. La maîtrise des paramètres de type, des contraintes et des patterns associés est devenue essentielle pour tout développeur Go professionnel. Les questions d'entretien sur ce sujet évaluent non seulement la connaissance de la syntaxe, mais aussi la capacité à reconnaître quand les génériques apportent une réelle valeur au code.

Partager

Articles similaires