Tri des structures en Go : comment écrire court sans se prendre la tête
\n\nImaginez : vous avez une slice de structures et vous devez la trier selon n'importe quel champ. Nom, âge, salaire, date de naissance — tout ce que vous voulez. Sur Stack Overflow, la question « What is the shortest way to simply sort an array of structs by (arbitrary) field names? » a recueilli des milliers de vues. Et ce n'est pas un hasard. Go n'a pas de méthode « magique » intégrée comme list.sort(key=lambda x: x.field) en Python. Mais il existe une manière élégante et puissante — sort.Slice. Dans cet article, nous ne nous contentons pas de montrer le code, nous regardons sous le capot : comment fonctionne le tri, ce que fait le runtime et comment éviter les pièges courants.
Problème : « Pourquoi ne peut-on pas simplement écrire sort() en Go ? »
\n\nSi vous venez de Python, JavaScript ou Ruby, vous êtes habitué à une seule ligne :
\nusers.sort(key=lambda u: u.age)\nEn Go, ce luxe n'existe pas. Le typage strict et l'absence de génériques (avant Go 1.18) obligeaient à écrire une implémentation séparée de sort.Interface pour chaque type de structure. C'était lourd. Par exemple :
type ByAge []User\nfunc (a ByAge) Len() int { return len(a) }\nfunc (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }\n\nfunc main() {\n users := []User{{"Alice", 30}, {"Bob", 25}}\n sort.Sort(ByAge(users))\n}\nÇa fonctionne, mais pour chaque champ — un nouveau type. Et s'il y a 10 champs ? Le code devient une usine à gaz. On veut quelque chose de plus compact.
\n\nSolution : sort.Slice — votre kit de gentleman
\n\nDepuis Go 1.8, le package sort contient la fonction Slice. Elle prend une slice et une fonction anonyme (fermeture) qui définit l'ordre. Voici la manière la plus courte :
sort.Slice(users, func(i, j int) bool {\n return users[i].Age < users[j].Age\n})\nVoilà. Trois lignes. Sans déclaration de nouveaux types, sans méthodes superflues. Vous voulez trier par nom ? Changez le champ :
\nsort.Slice(users, func(i, j int) bool {\n return users[i].Name < users[j].Name\n})\nEt si vous voulez par ordre décroissant ? Changez simplement le signe :
\nsort.Slice(users, func(i, j int) bool {\n return users[i].Age > users[j].Age // maintenant par ordre décroissant\n})\nMagnifique. Mais comment ça marche ? Et quel est le piège ?
\n\nSous le capot : sort.Slice et les fonctions anonymes
\n\nAnalysons ce qui se passe lorsque vous appelez sort.Slice. La fonction est déclarée ainsi :
func Slice(slice interface{}, less func(i, j int) bool)\nLa première chose qui saute aux yeux est interface{}. La fonction accepte n'importe quelle slice. Comment connaît-elle sa longueur et comment permute-t-elle les éléments ? La réponse — via la réflexion (package reflect).
À l'intérieur, sort.Slice fait ce qui suit :
- \n
- Vérifie qu'une slice a été passée. Si vous passez autre chose qu'une slice (par exemple un tableau ou une structure), le programme paniquera à l'exécution. \n
- Extrait la longueur via
reflect.ValueOf(slice).Len(). \n - Crée un objet auxiliaire qui implémente dynamiquement l'interface
sort.Interface. Cet objet stocke une référence à la slice d'origine (via la réflexion) et à votre fonctionless. \n - Lance l'algorithme de tri standard (quicksort du package
sort). Lorsque l'algorithme doit comparer deux éléments, il appelle votre fonctionless. Lorsqu'il doit les échanger, il utilisereflect.Swapper(slice). \n
Point clé : votre fonction less capture la slice d'origine du contexte externe. C'est une fermeture (closure). Le compilateur place la variable users sur le tas pour qu'elle soit accessible même après la sortie de la fonction m