Vergleich von Slices in Go: Wie Sie Leistung nicht verlieren und nicht verrückt werden
\n\nStellen Sie sich vor: Sie schreiben einen hochbelasteten Dienst in Go und müssen zwei Byte-Slices oder String-Slices vergleichen. Was könnte einfacher sein? Aber der Teufel steckt, wie immer, im Detail. Auf Stack Overflow hat die Frage "Checking the equality of two slices" tausende Aufrufe gesammelt – und das nicht ohne Grund. In diesem Artikel analysieren wir, welche Fallstricke auf Entwickler warten, wie man einen korrekten Vergleich schreibt und, vor allem, wie man dies maximal schnell macht.
\n\nWir werden keine langweiligen akademischen Definitionen verwenden. Stattdessen – lebendige Beispiele, Benchmarks und praktische Tipps, die Sie noch heute anwenden können. Los geht's!
\n\nWarum ist ein einfacher Vergleich nicht immer der richtige?
\n\nIn Go ist ein Slice kein Array. Es ist ein Deskriptor, der einen Zeiger auf das zugrunde liegende Array, die Länge und die Kapazität enthält. Daher funktioniert der Operator == für Slices nicht. Der Versuch, slice1 == slice2 zu schreiben, führt zu einem Kompilierungsfehler:
invalid operation: slice1 == slice2 (slice can only be compared to nil)\n\nDas ist kein Bug, sondern ein Feature. Die Go-Entwickler haben den direkten Vergleich von Slices bewusst verboten, da dies zu nicht offensichtlichen Konsequenzen führen kann. Zum Beispiel können zwei Slices auf dasselbe zugrunde liegende Array verweisen, aber unterschiedliche Längen haben. Oder umgekehrt – unterschiedliche Arrays, aber identische Daten.
\n\nWas also tun? Es gibt mehrere Ansätze, und wir werden jeden davon betrachten.
\n\nMethode 1: Manuelle Schleife (einfach, aber zuverlässig)
\n\nDer offensichtlichste Weg ist, eine Funktion zu schreiben, die alle Elemente durchläuft und sie einzeln vergleicht. Dies gibt Ihnen die volle Kontrolle über den Prozess.
\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\nVorteile: Transparenz, keine Abhängigkeiten, Anpassungsmöglichkeit (z. B. Vergleich mit Toleranz für float).
\nNachteile: Muss für jeden Datentyp geschrieben werden.
Methode 2: reflect.DeepEqual (universell, aber langsam)
\n\nIn der Standardbibliothek gibt es die Funktion reflect.DeepEqual, die beliebige Datenstrukturen vergleichen kann – Slices, Maps, Strukturen. Das sieht so aus:
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\nScheint ideal. Aber es gibt einen Haken: reflect.DeepEqual verwendet Reflektion, und das ist immer teuer. Bei kleinen Slices ist der Unterschied unbemerkt, aber bei tausenden Elementen sinkt die Leistung um ein Vielfaches.
Wichtig: reflect.DeepEqual betrachtet nil und einen leeren Slice als unterschiedlich! Das kann eine Überraschung sein:
var a []int = nil\nb := []int{}\nfmt.Println(reflect.DeepEqual(a, b)) // false!\n\nMethode 3: bytes.Equal für Byte-Slices (optimal)
\n\nWenn Sie mit []byte arbeiten, ist die beste Wahl die Funktion bytes.Equal aus der Standardbibliothek. Sie ist in Assembler geschrieben und maximal optimiert:
import "bytes"\n\nfunc main() {\n a := []byte("hello")\n b := []byte("hello")\n fmt.Println(bytes.Equal(a, b)) // true\n}\n\nDies ist der schnellste Weg für Byte-Slices. Intern verwendet sie memcmp auf C-Ebene, was einen enormen Leistungsschub bringt.
Methode 4: Vergleich mit crypto/subtle (für Kryptographie)
\n\nEine besondere Erwähnung verdient das Paket crypto/subtle. Seine Funktion ConstantTimeCompare ist für den Vergleich geheimer Daten (Passwörter, Token) ohne Zeitverlust ausgelegt. Ein normaler Vergleich kann anfällig für Timing-Angriffe sein:
import "crypto/subtle"\n\nfunc