Voici la version blog de mes présentations à Google Open Source Live :
https://youtu.be/nr8EpUO9jhw?si=jlWTapr6NM6isLgt&embedable=true
et à GopherCon 2021 :
https://youtu.be/Pae9EeCdy8?si=M-87Eisb2nU1qmJ&embedable=true
\ La version 1.18 de Go ajoute une nouvelle fonctionnalité majeure au langage : le support de la programmation générique. Dans cet article, je ne vais pas décrire ce que sont les génériques ni comment les utiliser. Cet article traite de quand utiliser les génériques dans le code Go, et quand ne pas les utiliser.
\ Pour être clair, je vais fournir des lignes directrices générales, et non des règles strictes. Utilisez votre propre jugement. Mais si vous n'êtes pas sûr, je vous recommande d'utiliser les lignes directrices discutées ici.
Commençons par une ligne directrice générale pour la programmation Go : écrivez des programmes Go en écrivant du code, pas en définissant des types. En ce qui concerne les génériques, si vous commencez à écrire votre programme en définissant des contraintes de paramètres de type, vous êtes probablement sur la mauvaise voie. Commencez par écrire des fonctions. Il est facile d'ajouter des paramètres de type plus tard lorsqu'il est clair qu'ils seront utiles.
Cela dit, examinons les cas pour lesquels les paramètres de type peuvent être utiles.
Un cas est lors de l'écriture de fonctions qui opèrent sur les types de conteneurs spéciaux définis par le langage : les tranches (slices), les cartes (maps) et les canaux (channels). Si une fonction a des paramètres avec ces types, et que le code de la fonction ne fait aucune hypothèse particulière sur les types d'éléments, alors il peut être utile d'utiliser un paramètre de type.
\ Par exemple, voici une fonction qui retourne une tranche de toutes les clés dans une carte de n'importe quel type :
// MapKeys returns a slice of all the keys in m. // The keys are not returned in any particular order. func MapKeys[Key comparable, Val any](m map[Key]Val) []Key { s := make([]Key, 0, len(m)) for k := range m { s = append(s, k) } return s }
\ Ce code ne suppose rien sur le type de clé de la carte, et n'utilise pas du tout le type de valeur de la carte. Il fonctionne pour n'importe quel type de carte. Cela en fait un bon candidat pour l'utilisation de paramètres de type.
\ L'alternative aux paramètres de type pour ce genre de fonction est généralement d'utiliser la réflexion, mais c'est un modèle de programmation plus maladroit, qui n'est pas vérifié statiquement au moment de la compilation, et qui est souvent plus lent à l'exécution.
Un autre cas où les paramètres de type peuvent être utiles est pour les structures de données à usage général. Une structure de données à usage général est quelque chose comme une tranche ou une carte, mais qui n'est pas intégrée au langage, comme une liste chaînée ou un arbre binaire.
\ Aujourd'hui, les programmes qui ont besoin de telles structures de données font généralement l'une des deux choses suivantes : les écrire avec un type d'élément spécifique, ou utiliser un type d'interface. Remplacer un type d'élément spécifique par un paramètre de type peut produire une structure de données plus générale qui peut être utilisée dans d'autres parties du programme, ou par d'autres programmes. Remplacer un type d'interface par un paramètre de type peut permettre de stocker les données plus efficacement, économisant ainsi des ressources mémoire ; cela peut également permettre au code d'éviter les assertions de type et d'être entièrement vérifié au moment de la compilation.
\ Par exemple, voici une partie de ce à quoi pourrait ressembler une structure de données d'arbre binaire utilisant des paramètres de type :
// Tree is a binary tree. type Tree[T any] struct { cmp func(T, T) int root *node[T] } // A node in a Tree. type node[T any] struct { left, right *node[T] val T } // find returns a pointer to the node containing val, // or, if val is not present, a pointer to where it // would be placed if added. func (bt *Tree[T]) find(val T) **node[T] { pl := &bt.root for *pl != nil { switch cmp := bt.cmp(val, (*pl).val); { case cmp < 0: pl = &(*pl).left case cmp > 0: pl = &(*pl).right default: return pl } } return pl } // Insert inserts val into bt if not already there, // and reports whether it was inserted. func (bt *Tree[T]) Insert(val T) bool { pl := bt.find(val) if *pl != nil { return false } *pl = &node[T]{val: val} return true }
\ Chaque nœud de l'arbre contient une valeur du paramètre de type T. Lorsque l'arbre est instancié avec un argument de type particulier, les valeurs de ce type seront stockées directement dans les nœuds. Elles ne seront pas stockées comme des types d'interface.
\ C'est une utilisation raisonnable des paramètres de type car la structure de données Tree, y compris le code dans les méthodes, est largement indépendante du type d'élément T.
\ La structure de données Tree doit savoir comment comparer les valeurs du type d'élément T ; elle utilise une fonction de comparaison passée en paramètre pour cela. Vous pouvez le voir à la quatrième ligne de la méthode find, dans l'appel à bt.cmp. À part cela, le paramètre de type n'a aucune importance.
L'exemple Tree illustre une autre ligne directrice générale : lorsque vous avez besoin de quelque chose comme une fonction de comparaison, préférez une fonction à une méthode.
\ Nous aurions pu définir le type Tree de telle sorte que le type d'élément soit obligé d'avoir une méthode Compare ou Less. Cela se ferait en écrivant une contrainte qui exige la méthode, ce qui signifie que tout argument de type utilisé pour instancier le type Tree devrait avoir cette méthode.
\ Une conséquence serait que quiconque veut utiliser Tree avec un type de données simple comme int devrait définir son propre type d'entier et écrire sa propre méthode de comparaison. Si nous définissons Tree pour prendre une fonction de comparaison, comme dans le code montré ci-dessus, alors il est facile de passer la fonction souhaitée. Il est tout aussi facile d'écrire cette fonction de comparaison que d'écrire une méthode.
\ Si le type d'élément Tree a déjà une méthode Compare, alors nous pouvons simplement utiliser une expression de méthode comme ElementType.Compare comme fonction de comparaison.
\ Pour le dire autrement, il est beaucoup plus simple de transformer une méthode en fonction que d'ajouter une méthode à un type. Donc, pour les types de données à usage général, préférez une fonction plutôt que d'écrire une contrainte qui exige une méthode.
Un autre cas où les paramètres de type peuvent être utiles est lorsque différents types doivent implémenter une méthode commune, et que les implémentations pour les différents types sont toutes identiques.
\ Par exemple, considérez l'interface sort.Interface de la bibliothèque standard. Elle exige qu'un type implémente trois méthodes : Len, Swap et Less.
\ Voici un exemple de type générique SliceFn qui implémente sort.Interface pour n'importe quel type de tranche :
// SliceFn implements sort.Interface for a slice of T. type SliceFn[T any] struct { s []T less func(T, T) bool } func (s SliceFn[T]) Len() int { return len(s.s) } func (s SliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } func (s SliceFn[T]) Less(i, j int) bool { return s.less(s.s[i], s.s[j]) }
\ Pour n'importe quel type de tranche, les méthodes Len et Swap sont exactement les mêmes. La méthode Less nécessite une comparaison, qui est la partie Fn du nom SliceFn. Comme avec l'exemple Tree précédent, nous passerons une fonction lors de la création d'un SliceFn.
\ Voici comment utiliser SliceFn pour trier n'importe quelle tranche en utilisant une fonction de comparaison :
// SortFn sorts s in place using a comparison function. func SortFn[T any](s []T, less func(T, T) bool) { sort.Sort(SliceFn[T]{s, less}) }
\ C'est similaire à la fonction de bibliothèque standard sort.Slice, mais la fonction de comparaison est écrite en utilisant des valeurs plutôt que des indices de tranche.
\ L'utilisation de paramètres de type pour ce genre de code est appropriée car les méthodes sont exactement les mêmes pour tous les types de tranches.
\ (Je dois mentionner que Go 1.19 – pas 1.18 – inclura très probablement une fonction générique pour trier une tranche en utilisant une fonction de comparaison, et cette fonction générique n'utilisera très probablement pas sort.Interface. Voir la proposition #47619. Mais le point général reste vrai même si cet exemple spécifique ne sera très probablement pas utile : il est raisonnable d'utiliser des paramètres de type lorsque vous devez implémenter des méthodes qui sont identiques pour tous les types pertinents.)
Parlons maintenant de l'autre côté de la question : quand ne pas utiliser les paramètres de type.
Comme nous le savons tous, Go a des types d'interface. Les types d'interface permettent une sorte de programmation générique.
\ Par exemple, l'interface io.Reader largement utilisée fournit un mécanisme générique pour lire des données à partir de n'importe quelle valeur qui contient des informations (par exemple, un fichier) ou qui produit des informations (par exemple, un générateur de nombres aléatoires). Si tout ce que vous avez besoin de faire avec une valeur d'un certain type est d'appeler une méthode sur cette valeur, utilisez un type d'interface, pas un paramètre de type. io.Reader est facile à lire, efficace et efficiente. Il n'est pas nécessaire d'utiliser un paramètre de type pour lire des données à partir d'une valeur en appelant la méthode Read.
\ Par exemple, il pourrait être tentant de changer la première signature de fonction ici, qui utilise juste un type d'interface, en la deuxième version, qui utilise un paramètre de type.
func ReadSome(r io.Reader) ([]byte, error) func ReadSome[T io.Reader](r T) ([]byte, error)
\ Ne faites pas ce genre de changement. Omettre le paramètre de type rend la fonction plus facile à écrire, plus facile à lire, et le temps d'exécution sera probablement le même.
\ Il est important de souligner ce dernier point. Bien qu'il soit possible d'implémenter les génériques de plusieurs façons différentes, et que les implémentations changeront et s'amélioreront avec le temps, l'implémentation utilisée dans Go 1.18 traitera dans de nombreux cas les valeurs dont le type est un paramètre de type de la même manière que les valeurs dont le type est un type d'interface. Cela signifie que l'utilisation d'un paramètre de type ne sera généralement pas plus rapide que l'utilisation d'un type d'interface. Donc, ne passez pas des types d'interface aux paramètres de type juste pour la vitesse, car cela ne s'exécutera probablement pas plus rapidement.
\
Lorsque vous décidez d'utiliser un paramètre de type ou un type d'interface, considérez l'implémentation des


