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 un slice 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 de TestXxx)
  • La fonction prend *testing.F en type d’entrée (à la place de *testing.T)
  • On utilise f.Fuzz (à la place de t.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 de interface{} : 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 !