Les générics Go : cas d'utilisation et modèles
Code réutilisable et typé sécurisé avec les généricités de Go
Les générics en Go représentent l’une des fonctionnalités de langage les plus importantes ajoutées depuis Go 1.0. Introduits avec Go 1.18, les générics permettent d’écrire du code type-safe, réutilisable qui fonctionne avec plusieurs types sans sacrifier les performances ou la clarté du code.
Cet article explore des cas d’utilisation pratiques, des schémas courants et des bonnes pratiques pour utiliser les générics dans vos programmes Go. Si vous êtes nouveau en Go ou si vous avez besoin d’un rappel sur les fondamentaux, consultez notre Feuille de rappel Go pour les constructions linguistiques et la syntaxe essentielles.

Comprendre les générics en Go
Les générics en Go vous permettent d’écrire des fonctions et des types paramétrés par des paramètres de type. Cela élimine le besoin de duplication de code lorsque vous avez besoin de la même logique pour fonctionner avec différents types, tout en maintenant la sécurité de type à la compilation.
Syntaxe de base
La syntaxe des générics utilise des crochets pour déclarer les paramètres de type :
// Fonction générique
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
// Utilisation
maxInt := Max(10, 20)
maxString := Max("apple", "banana")
Contraintes de type
Les contraintes de type spécifient quels types peuvent être utilisés avec votre code générique :
any: Tout type (équivalent àinterface{})comparable: Types qui supportent les opérateurs==et!=- Contraintes d’interface personnalisées : Définissez vos propres exigences
// Utilisation d'une contrainte personnalisée
type Numeric interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Sum[T Numeric](numbers []T) T {
var sum T
for _, n := range numbers {
sum += n
}
return sum
}
Cas d’utilisation courants
1. Structures de données génériques
L’un des cas d’utilisation les plus convaincants des générics est la création de structures de données réutilisables :
// Pile générique
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
}
// Utilisation
intStack := NewStack[int]()
intStack.Push(42)
intStack.Push(100)
stringStack := NewStack[string]()
stringStack.Push("hello")
2. Utilitaires de tranches
Les générics rendent facile l’écriture de fonctions de manipulation de tranches réutilisables :
// Fonction Map
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
}
// Fonction Filter
func Filter[T any](slice []T, fn func(T) bool) []T {
var result []T
for _, v := range slice {
if fn(v) {
result = append(result, v)
}
}
return result
}
// Fonction Reduce
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
// Utilisation
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(n int) int { return n * 2 })
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
sum := Reduce(numbers, 0, func(acc, n int) int { return acc + n })
3. Utilitaires génériques pour les maps
Le travail avec les maps devient plus sûr en termes de type avec les générics :
// Obtenir les clés d'une map sous forme de tranche
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
}
// Obtenir les valeurs d'une map sous forme de tranche
func Values[K comparable, V any](m map[K]V) []V {
values := make([]V, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
}
// Obtenir une valeur d'une map avec une valeur par défaut
func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultValue V) V {
if v, ok := m[key]; ok {
return v
}
return defaultValue
}
4. Schéma générique Option
Le schéma Option devient plus élégant avec les générics :
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]) IsNone() bool {
return o.value == nil
}
func (o Option[T]) Unwrap() T {
if o.value == nil {
panic("attempted to unwrap None value")
}
return *o.value
}
func (o Option[T]) UnwrapOr(defaultValue T) T {
if o.value == nil {
return defaultValue
}
return *o.value
}
Schémas avancés
Composition des contraintes
Vous pouvez composer des contraintes pour créer des exigences plus spécifiques :
type Addable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64 | string
}
type Multiplicable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
type Numeric interface {
Addable
Multiplicable
}
func Multiply[T Multiplicable](a, b T) T {
return a * b
}
Interfaces génériques
Les interfaces peuvent également être génériques, permettant des abstractions puissantes :
type Repository[T any, ID comparable] interface {
FindByID(id ID) (T, error)
Save(entity T) error
Delete(id ID) error
FindAll() ([]T, error)
}
// Implémentation
type InMemoryRepository[T any, ID comparable] struct {
data map[ID]T
}
func NewInMemoryRepository[T any, ID comparable]() *InMemoryRepository[T, ID] {
return &InMemoryRepository[T, ID]{
data: make(map[ID]T),
}
}
func (r *InMemoryRepository[T, ID]) FindByID(id ID) (T, error) {
if entity, ok := r.data[id]; ok {
return entity, nil
}
var zero T
return zero, fmt.Errorf("entity not found")
}
Inférence de type
L’inférence de type de Go permet souvent d’omettre les paramètres de type explicites :
// Inférence de type en action
numbers := []int{1, 2, 3, 4, 5}
// Aucun besoin de spécifier [int] - Go l'infère
doubled := Map(numbers, func(n int) int { return n * 2 })
// Paramètres de type explicites lorsqu'ils sont nécessaires
result := Map[int, string](numbers, strconv.Itoa)
Bonnes pratiques
1. Commencez simple
N’utilisez pas trop les générics. Si une interface simple ou un type concret suffit, préférez-les pour une meilleure lisibilité :
// Préférez ceci pour les cas simples
func Process(items []Processor) {
for _, item := range items {
item.Process()
}
}
// Trop générique - évitez
func Process[T Processor](items []T) {
for _, item := range items {
item.Process()
}
}
2. Utilisez des noms de contraintes significatifs
Nommez vos contraintes clairement pour communiquer l’intention :
// Bon
type Sortable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64 | string
}
// Moins clair
type T interface {
int | string
}
3. Documentez les contraintes complexes
Lorsque les contraintes deviennent complexes, ajoutez une documentation :
// Numeric représente les types qui supportent les opérations arithmétiques.
// Cela inclut tous les types entiers et à virgule flottante.
type Numeric interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
4. Pensez aux performances
Les générics sont compilés en types concrets, donc il n’y a aucun surcoût à l’exécution. Cependant, soyez attentif à la taille du code si vous instanciez de nombreuses combinaisons de types :
// Chaque combinaison crée du code séparé
var intStack = NewStack[int]()
var stringStack = NewStack[string]()
var floatStack = NewStack[float64]()
Applications réelles
Constructeurs de requêtes de base de données
Les générics sont particulièrement utiles lors de la création de constructeurs de requêtes de base de données ou lors de l’utilisation d’ORM. Lorsque vous travaillez avec des bases de données en Go, vous pouvez trouver notre comparaison des ORM pour PostgreSQL en Go utile pour comprendre comment les générics peuvent améliorer la sécurité de type dans les opérations de base de données.
type QueryBuilder[T any] struct {
table string
where []string
}
func (qb *QueryBuilder[T]) Where(condition string) *QueryBuilder[T] {
qb.where = append(qb.where, condition)
return qb
}
func (qb *QueryBuilder[T]) Find() ([]T, error) {
// Implémentation
return nil, nil
}
Gestion de configuration
Lors de la création d’applications CLI ou de systèmes de configuration, les générics peuvent aider à créer des chargeurs de configuration type-safe. Si vous construisez des outils en ligne de commande, notre guide sur la création d’applications CLI avec Cobra & Viper démontre comment les générics peuvent améliorer la gestion de configuration.
type ConfigLoader[T any] struct {
path string
}
func (cl *ConfigLoader[T]) Load() (T, error) {
var config T
// Charger et désérialiser la configuration
return config, nil
}
Bibliothèques d’utilitaires
Les générics brillent lors de la création de bibliothèques d’utilitaires qui fonctionnent avec divers types. Par exemple, lors de la génération de rapports ou du travail avec différents formats de données, les générics peuvent fournir une sécurité de type. Notre article sur la génération de rapports PDF en Go montre comment les générics peuvent être appliqués aux utilitaires de génération de rapports.
Code critique en termes de performance
Dans les applications sensibles aux performances comme les fonctions serverless, les générics peuvent aider à maintenir la sécurité de type sans surcoût à l’exécution. Lorsque vous considérez les choix de langage pour les applications serverless, comprendre les caractéristiques de performance est crucial. Notre analyse de la performance d’AWS Lambda en JavaScript, Python et Golang démontre comment la performance de Go, combinée aux générics, peut être avantageuse.
Pièges courants
1. Sur-contrainte des types
Évitez de rendre les contraintes trop restrictives lorsqu’elles ne le doivent pas :
// Trop restrictif
func Process[T int | string](items []T) { }
// Meilleur - plus flexible
func Process[T comparable](items []T) { }
2. Ignorer l’inférence de type
Laissez Go inférer les types lorsqu’il est possible :
// Types explicites inutiles
result := Max[int](10, 20)
// Meilleur - laissez Go inférer
result := Max(10, 20)
3. Oublier les valeurs par défaut
N’oubliez pas que les types génériques ont des valeurs par défaut :
func Get[T any](slice []T, index int) (T, bool) {
if index < 0 || index >= len(slice) {
var zero T // Important : retournez la valeur par défaut
return zero, false
}
return slice[index], true
}
Conclusion
Les générics en Go offrent un moyen puissant d’écrire du code type-safe et réutilisable sans sacrifier les performances ou la lisibilité. En comprenant la syntaxe, les contraintes et les schémas courants, vous pouvez utiliser les générics pour réduire la duplication de code et améliorer la sécurité de type dans vos applications Go.
N’oubliez pas d’utiliser les générics avec discernement – toutes les situations ne nécessitent pas leur utilisation. Lorsque vous hésitez, préférez des solutions plus simples comme les interfaces ou les types concrets. Cependant, lorsque vous avez besoin de travailler avec plusieurs types tout en maintenant la sécurité de type, les générics sont un excellent outil dans votre boîte à outils Go.
Alors que Go continue d’évoluer, les générics deviennent une fonctionnalité essentielle pour construire des applications modernes et type-safe. Qu’il s’agisse de construire des structures de données, des bibliothèques d’utilitaires ou des abstractions complexes, les générics peuvent vous aider à écrire un code plus propre et plus maintenable.
Liens utiles et articles connexes
- Tutoriel sur les générics en Go - Blog officiel Go
- Proposition des paramètres de type
- Documentation sur les générics en Go
- Go efficace - Générics
- Notes de publication Go 1.18 - Générics
- Spécification du langage Go - Paramètres de type
- Jouet de générics Go
- Quand utiliser les générics - Blog Go
- Feuille de rappel Go
- Créer des applications CLI en Go avec Cobra & Viper
- Comparaison des ORM pour PostgreSQL en Go : GORM vs Ent vs Bun vs sqlc
- Générer des PDF en GO - Bibliothèques et exemples
- Performance d’AWS Lambda : JavaScript vs Python vs Golang