التكرار على map في Go: كيف لا تخطو على أشواك (ولا تفقد البيانات)
\n\nإذا كنت قد كتبت بلغة Go من قبل، فبالتأكيد واجهت map - واحدة من أقوى هياكل البيانات وأكثرها مكراً. قد يبدو الأمر بسيطاً: تأخذ range، تمر عبر المفاتيح، وتحصل على القيم. لكن الشيطان، كما هو الحال دائماً، في التفاصيل. سؤال "Iterating over all the keys of a map" على Stack Overflow حصد آلاف المشاهدات، وهذا ليس من قبيل الصدفة.
اليوم سنحلل أهم الأخطاء عند التكرار على map في Go التي يرتكبها حتى المطورون ذوو الخبرة. ستتعرف لماذا ترتيب الاجتياز هو يانصيب، وكيفية العمل بأمان مع map في goroutines، وماذا تفعل إذا كان map فجأة nil. هيا بنا!
الخطأ رقم 1: الاعتماد على الترتيب (أو "أين مفاتيحي؟")
\n\nأكثر فخ شائع هو الاعتقاد بأن range سيعيد المفاتيح بنفس الترتيب الذي أضفتها به. في Go هذا غير صحيح. ترتيب التكرار على map هو غير حتمي بشكل أساسي.
انظر إلى هذا الكود:
\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("التكرار", i+1)\n for k, v := range m {\n fmt.Printf(" %s: %d\\n", k, v)\n }\n }\n}\n\n\nعلى الأرجح، كل تشغيل سيعطي ترتيباً مختلفاً. هذا ليس خطأ، بل ميزة: Go تعمد إلى عشوائية الترتيب حتى لا يعتاد المطورون عليه ولا يبنون منطقاً على تسلسل عشوائي.
\n\nكيف تتجنبه؟
\n- \n
- لا تعتمد على الترتيب - هذه هي القاعدة الأساسية. \n
- إذا كان الترتيب مهماً، استخدم
[]stringلتخزين المفاتيح بشكل منفصل وقم بترتيبها: \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\nنصيحة: إذا كنت بحاجة للحفاظ على ترتيب الإدراج، فكر في استخدام مكتبة github.com/elliotchance/orderedmap أو اكتب غلافك الخاص بناءً على هيكلين (map + slice).
الخطأ رقم 2: التكرار مع الكتابة المتزامنة (data race)
\n\nفي Go، map غير آمن للوصول المتزامن. إذا كانت إحدى goroutines تقرأ (تكرر) map بينما تكتب أخرى فيها في نفس الوقت، ستحصل على fatal error: concurrent map iteration and map write.
\n\nمثال على خطأ فادح:
\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\nهذا الكود سينهار مع panic. وليس دائماً - قد يظهر شرط السباق (race condition) في التشغيل رقم 1000.
\n\nكيف تتجنبه؟
\n\nهناك ثلاث طرق مجربة:
\n\n- \n
- sync.Mutex - الميوتكس الكلاسيكي لمزامنة الوصول: \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