Sortierung von Strukturen in Go: Wie man kurz schreibt und sich nicht das Gehirn verknotet
\n\nStellen Sie sich vor: Sie haben ein Slice von Strukturen und müssen es nach einem beliebigen Feld sortieren. Name, Alter, Gehalt, Geburtsdatum – was auch immer. Auf Stack Overflow hat die Frage „What is the shortest way to simply sort an array of structs by (arbitrary) field names?“ tausende Aufrufe gesammelt. Und das ist kein Zufall. In Go gibt es keine eingebaute „magische“ Methode wie list.sort(key=lambda x: x.field) aus Python. Aber es gibt einen eleganten und mächtigen Weg – sort.Slice. In diesem Artikel zeigen wir nicht nur den Code, sondern schauen unter die Haube: wie die Sortierung funktioniert, was die Runtime macht und wie man typische Fallstricke vermeidet.
Das Problem: „Warum kann man in Go nicht einfach sort() schreiben?“
\n\nWenn Sie von Python, JavaScript oder Ruby kommen, sind Sie eine einzige Zeile gewohnt:
\nusers.sort(key=lambda u: u.age)\nDiesen Luxus gibt es in Go nicht. Starke Typisierung und das Fehlen von Generics (bis Go 1.18) zwangen dazu, für jeden Strukturtyp eine separate Implementierung von sort.Interface zu schreiben. Das war umständlich. Zum Beispiel:
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}\nEs funktioniert, aber für jedes Feld ein neuer Typ. Und wenn es 10 Felder sind? Der Code wird zu einem langen Text. Man wünscht sich etwas Kompakteres.
\n\nDie Lösung: sort.Slice – Ihr Allround-Werkzeug
\n\nAb Go 1.8 gibt es im Paket sort die Funktion Slice. Sie nimmt ein Slice und eine anonyme Funktion (Closure) entgegen, die die Reihenfolge bestimmt. Hier ist er – der kürzeste Weg:
sort.Slice(users, func(i, j int) bool {\n return users[i].Age < users[j].Age\n})\nDas war's. Drei Zeilen. Ohne Deklaration neuer Typen, ohne überflüssige Methoden. Möchten Sie nach Namen sortieren? Wir ändern das Feld:
\nsort.Slice(users, func(i, j int) bool {\n return users[i].Name < users[j].Name\n})\nUnd wenn es absteigend sein soll? Einfach das Vorzeichen ändern:
\nsort.Slice(users, func(i, j int) bool {\n return users[i].Age > users[j].Age // jetzt absteigend\n})\nWunderschön. Aber wie funktioniert das? Und wo ist der Haken?
\n\nUnter der Haube: sort.Slice und anonyme Funktionen
\n\nLassen Sie uns analysieren, was passiert, wenn Sie sort.Slice aufrufen. Die Funktion ist so deklariert:
func Slice(slice interface{}, less func(i, j int) bool)\nDas erste, was ins Auge fällt, ist interface{}. Die Funktion akzeptiert jedes Slice. Wie erfährt sie seine Länge und wie vertauscht sie die Elemente? Die Antwort – über Reflection (Paket reflect).
Im Inneren macht sort.Slice Folgendes:
- \n
- Überprüft, ob ein Slice übergeben wurde. Wenn kein Slice übergeben wird (z. B. ein Array oder eine Struktur), gerät das Programm zur Laufzeit in Panik. \n
- Ermittelt die Länge über
reflect.ValueOf(slice).Len(). \n - Erstellt ein Hilfsobjekt, das das Interface
sort.Interfacedynamisch implementiert. Dieses Objekt speichert einen Verweis auf das ursprüngliche Slice (über Reflection) und auf Ihreless-Funktion. \n - Startet den Standard-Sortieralgorithmus (Quicksort aus dem Paket
sort). Wenn der Algorithmus zwei Elemente vergleichen muss, ruft er Ihreless-Funktion auf. Wenn er Elemente vertauschen muss, verwendet erreflect.Swapper(slice). \n
Der entscheidende Punkt: Ihre less-Funktion erfasst das ursprüngliche Slice aus dem äußeren Kontext. Dies ist ein Closure. Der Compiler platziert die Variable users auf dem Heap, damit sie auch nach dem Verlassen der Funktion m verfügbar ist.