Iteration über map in Go: Wie man nicht in die Falle tappt (und keine Daten verliert)
\n\nWenn Sie jemals in Go programmiert haben, sind Sie sicherlich auf map gestoßen – eine der mächtigsten und tückischsten Datenstrukturen. Man könnte meinen, was gibt es Einfacheres: range nehmen, über die Schlüssel iterieren, Werte erhalten. Aber der Teufel steckt, wie immer, im Detail. Die Frage "Iterating over all the keys of a map" auf Stack Overflow hat tausende Aufrufe gesammelt, und das nicht ohne Grund.
Heute analysieren wir die Top-Fehler bei der Iteration über map in Go, die selbst erfahrene Entwickler machen. Sie erfahren, warum die Durchlaufreihenfolge eine Lotterie ist, wie man sicher mit map in Goroutinen arbeitet und was zu tun ist, wenn map plötzlich nil ist. Los geht's!
Fehler Nr. 1: Hoffnung auf Reihenfolge (oder "Wo sind meine Schlüssel?")
\n\nDie häufigste Falle ist die Annahme, dass range die Schlüssel in derselben Reihenfolge zurückgibt, in der Sie sie hinzugefügt haben. In Go ist das nicht der Fall. Die Iterationsreihenfolge über map ist grundsätzlich nichtdeterministisch.
Sehen Sie sich diesen Code an:
\n\npackage main\n\nimport "fmt"\n\nfunc main() {\n m := map[string]int{\n "apple": 1,\n "banana": 2,\n "cherry": 3,\n }\n\n for i := 0; i < 3; i++ {\n fmt.Println("Iteration", i+1)\n for k, v := range m {\n fmt.Printf(" %s: %d\\n", k, v)\n }\n }\n}\n\n\nWahrscheinlich wird jeder Durchlauf eine andere Reihenfolge ergeben. Das ist kein Bug, sondern ein Feature: Go randomisiert die Reihenfolge absichtlich, damit Entwickler sich nicht daran gewöhnen und keine Logik auf eine zufällige Sequenz aufbauen.
\n\nWie vermeidet man das?
\n- \n
- Verlassen Sie sich nicht auf die Reihenfolge – das ist die Grundregel. \n
- Wenn die Reihenfolge wichtig ist, verwenden Sie
[]stringzur separaten Speicherung der Schlüssel und sortieren Sie diese: \n
package main\n\nimport (\n "fmt"\n "sort"\n)\n\nfunc main() {\n m := map[string]int{"apple": 1, "banana": 2, "cherry": 3}\n\n keys := make([]string, 0, len(m))\n for k := range m {\n keys = append(keys, k)\n }\n sort.Strings(keys)\n\n for _, k := range keys {\n fmt.Printf("%s: %d\\n", k, m[k])\n }\n}\n\n\nTipp: Wenn Sie die Einfügereihenfolge beibehalten müssen, ziehen Sie die Bibliothek github.com/elliotchance/orderedmap in Betracht oder schreiben Sie Ihren eigenen Wrapper basierend auf zwei Strukturen (map + slice).
Fehler Nr. 2: Iteration mit gleichzeitigem Schreiben (data race)
\n\nIn Go ist map nicht sicher für konkurrierenden Zugriff. Wenn eine Goroutine eine map liest (iteriert) und eine andere gleichzeitig in sie schreibt, erhalten Sie einen fatal error: concurrent map iteration and map write.
\n\nBeispiel für einen fatalen Fehler:
\n\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc main() {\n m := make(map[int]int)\n\n go func() {\n for i := 0; i < 100; i++ {\n m[i] = i\n }\n }()\n\n go func() {\n for k, v := range m {\n fmt.Println(k, v)\n }\n }()\n\n time.Sleep(time.Second)\n}\n\n\nDieser Code wird mit einer Panik abstürzen. Und nicht immer – eine race condition kann sich erst beim 1000. Durchlauf zeigen.
\n\nWie vermeidet man das?
\n\nEs gibt drei bewährte Methoden:
\n\n- \n
- sync.Mutex – klassischer Mutex zur Synchronisation des Zugriffs: \n
package main\n\nimport (\n "fmt"\n "sync"\n)\n\ntype SafeMap struct {\n mu sync.Mutex\n m map[int]int\n}\n\nfunc (s *SafeMap) Set(key, value int) {\n s.mu.Lock()\n defer s.mu.Unlock()\n s.m[key] = value\n}\n\nfunc (s *SafeMap) Iterate(f func(key, value int) bool) {\n s.mu.Lock()\n defer s.mu.Unlock()\n for k, v := range s.m {\n if !f(k, v) {\n break\n }\n }\n}\n\nfunc main() {\n sm := SafeMap{m: make(map[int]int)}\n sm.Set(1, 10)\n sm.Set(2, 20)\n\n sm.Iterate(func(k, v int) b