Comparaison de slices en Go : comment ne pas perdre en performance et ne pas devenir fou
\n\nImaginez la situation : vous écrivez un service à forte charge en Go, et vous devez comparer deux slices d'octets ou de chaînes. Cela semble simple, non ? Mais le diable, comme toujours, se cache dans les détails. Sur Stack Overflow, la question "Checking the equality of two slices" a recueilli des milliers de vues — et ce n'est pas pour rien. Dans cet article, nous allons voir quels pièges attendent le développeur, comment écrire une comparaison correcte et, surtout, comment le faire le plus rapidement possible.
\n\nNous n'utiliserons pas de définitions académiques ennuyeuses. À la place, des exemples concrets, des benchmarks et des conseils pratiques que vous pourrez appliquer dès aujourd'hui. C'est parti !
\n\nPourquoi une simple comparaison n'est-elle pas toujours correcte ?
\n\nEn Go, un slice n'est pas un tableau. C'est un descripteur qui contient un pointeur vers le tableau sous-jacent (underlying array), une longueur et une capacité. Par conséquent, l'opérateur == ne fonctionne pas pour les slices. Essayer d'écrire slice1 == slice2 entraînera une erreur de compilation :
invalid operation: slice1 == slice2 (slice can only be compared to nil)\n\nCe n'est pas un bug, mais une fonctionnalité. Les développeurs de Go ont délibérément interdit la comparaison directe des slices, car cela pourrait entraîner des conséquences imprévues. Par exemple, deux slices peuvent référencer le même tableau sous-jacent, mais avoir des longueurs différentes. Ou inversement — des tableaux différents, mais des données identiques.
\n\nAlors, que faire ? Il existe plusieurs approches, et nous allons examiner chacune d'elles.
\n\nMéthode 1 : Boucle manuelle (basique, mais fiable)
\n\nLa façon la plus évidente est d'écrire une fonction qui parcourt tous les éléments et les compare un par un. Cela donne un contrôle total sur le processus.
\n\nfunc EqualSlices(a, b []int) bool {\n if len(a) != len(b) {\n return false\n }\n for i := range a {\n if a[i] != b[i] {\n return false\n }\n }\n return true\n}\n\nAvantages : transparence, aucune dépendance, possibilité de personnalisation (par exemple, comparaison avec tolérance pour les floats).
\nInconvénients : il faut l'écrire pour chaque type de données.
Méthode 2 : reflect.DeepEqual (universel, mais lent)
\n\nDans la bibliothèque standard, il existe la fonction reflect.DeepEqual qui peut comparer n'importe quelle structure de données — slices, maps, structures. Cela ressemble à ceci :
import "reflect"\n\nfunc main() {\n a := []int{1, 2, 3}\n b := []int{1, 2, 3}\n fmt.Println(reflect.DeepEqual(a, b)) // true\n}\n\nCela semble idéal. Mais il y a un hic : reflect.DeepEqual utilise la réflexion, ce qui est toujours coûteux. Pour les petits slices, la différence est imperceptible, mais si vous avez des milliers d'éléments, les performances chuteront considérablement.
Important : reflect.DeepEqual considère également nil et un slice vide comme différents ! Cela peut être une surprise :
var a []int = nil\nb := []int{}\nfmt.Println(reflect.DeepEqual(a, b)) // false!\n\nMéthode 3 : bytes.Equal pour les slices d'octets (optimal)
\n\nSi vous travaillez avec []byte, le meilleur choix est la fonction bytes.Equal de la bibliothèque standard. Elle est écrite en assembleur et hautement optimisée :
import "bytes"\n\nfunc main() {\n a := []byte("hello")\n b := []byte("hello")\n fmt.Println(bytes.Equal(a, b)) // true\n}\n\nC'est la méthode la plus rapide pour les slices d'octets. En interne, elle utilise memcmp au niveau C, ce qui offre un énorme gain de performance.
Méthode 4 : Comparaison avec crypto/subtle (pour la cryptographie)
\n\nLe package crypto/subtle mérite une mention spéciale. Sa fonction ConstantTimeCompare est conçue pour comparer des données secrètes (mots de passe, jetons) sans fuite de temps. Une comparaison classique peut être vulnérable aux attaques temporelles (timing attacks) :
import "crypto/subtle"\n\nfunc