Generics
On peut définir la programmation générique comme un style de programmation qui permet de représenter des fonctions et des structures de données sous une forme générique, avec des types adaptés. Ca, c’est pour la théorie. Voyons un exemple.
Pour illustrer le principe, imaginons que nous avons besoins de calculer la somme des éléments d’un map
de int64
:
// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
Rien d’exceptionnel.
Maintenant, si nous avons besoin de faire de même pour un map
de float64
, notre fonction pourrait être quelque chose de ce genre :
// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
On constate la très grande similarité entre nos deux fonctions ; rien ne change à part le type.
Déclarer une fonction générique
Pour pouvoir utiliser des valeurs de plusieurs types, il faut écrire une fonction qui déclare des type parameters
en plus des paramètres de fonction habituels.
Chaque type parameter
a une contrainte qui permet de spécifier le type d’argument qui peut être accepté par la fonction.
Notre fonction générique peut se définir ainsi :
// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
On peut déclarer les contraintes comme une interface et modifier la fonction en conséquence :
type Number interface {
int64 | float64
}
// SumNumbers sums the values of map m. It supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
Cas d’usage
La généricité peut être utile pour les fonctions qui font :
- Trouver le plus petit/grand élément d’un
slice
- Trouver la déviation moyenne/standard d’un
slice
- Calculer l’union/intersection de
maps
- Trouver le chemin le plus court d’un nœud d’un graphe
- Appliquer une fonction de transformation à un
slice
/map
, qui retourne unslice
de résultat
Dans les cas plus spécifique à l’utilisation de la concurrence en Go, on pourrait avoir les cas d’usages suivant :
- Lire un canal avec un timeout
- Combiner deux canaux en un seul
- Appeler une liste de fonctions en parallèle, renvoyer un
slice
de résultat - Appeler une liste de fonctions, en utilisant un Context, retourner le résultat de la première fonction qui termine, annuler et nettoyer les autres goroutines
Aller plus loin
Les exemples et les explications sont tirés de ce tutoriel (et également plus complet).
Une introduction publiée sur le blog officiel de Go.
Les détails sont dans la release note.
Des explications sur le pourquoi, les cas d’usages classiques : Why Generics ?.
Fuzzing
Le fuzzing est l’injection de données aléatoires dans un test afin de tenter de trouver une vulnérabilités ou de détecter des potentielles entrées qui pourraient faire crasher un programme. Voyons un exemple de mise en œuvre.
Exemple
On prend une fonction qui inverse le sens d’une chaîne de caractères :
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
Cette fonction prend une string
en entrée, itère octet (byte
) par octet et renvoi la string
inversée.
Nota: ce code est basé sur la fonction stringutil.Reverse
de golang.org/x/example
.
Ajoutons un test unitaire :
package main
import (
"testing"
)
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
Il s’agit d’un simple test qui s’assure que la string
d’entrée est bien inversée.
Remplaçons maintenant ce test classique par un test en fuzzing :
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
Il y a des différences de syntaxes par rapport aux tests classiques :
- La nom de la fonction débute par
FuzzXxx
(à la place deTestXxx)
- La fonction prend
*testing.F
en type d’entrée (à la place de*testing.T
) - On utilise
f.Fuzz
(à la place det.Run
) qui prend comme paramètre une fonction à fuzzer qui a comme paramètres*testing.T
et le type à fuzzer
On peut ici identifier une limitation à ce type de test : il n’est pas possible de prédire la sortie puisque nous contrôlons pas l’entrée. Il faut donc s’appuyer sur d’autres propriétés pour réaliser nos tests, comme :
- Inverser deux fois l’entrée doit préserver l’entrée originale
- La chaîne inversée doit être une chaîne UTF-8 valide
Aller plus loin
Le tutoriel de go.dev (dont sont tiré les exemples ci-dessus), ainsi que la documentation officielle vous permettrons de creuser le sujet.
Workspaces
Cette nouvelle fonctionnalité permet de simplifier le travail sur de multiples paquets interdépendant en même temps.
Un workspace est défini par un fichier go.work
de cette forme :
go 1.18
use (
../foo/bar
./baz
)
replace example.com/foo v1.2.3 => example.com/bar v1.4.5
On peut alors initialiser un espace de travail dans le répertoire courant :
go work init
Aller plus loin
Autres ajouts
- Amélioration des performances de 20% sur Apple M1, ARM64, PowerPC64.
- Le type
any
comme alias deinterface{}
: l’issue
sur Github, un article de blog. - Un nouveau paquet
net/netip
avec un type d’adresse IP : documentation officielle. - Un paquet
debug/buildinfo
pour accéder aux informations d’un binaire compilé : documentation officielle.
Pour conclure
Voilà pour ce qui me paraît l’essentiel à savoir sur cette nouvelle version de Go. Vous trouverez l’intégralité des ajouts, des suppressions et des corrections dans la Release Note.
Codez bien !