التكرار على map في Go: 5 أخطاء قاتلة وكيفية تجنبها

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

التكرار على map في Go: كيف لا تخطو على أشواك (ولا تفقد البيانات)

\n\n

إذا كنت قد كتبت بلغة Go من قبل، فبالتأكيد واجهت map - واحدة من أقوى هياكل البيانات وأكثرها مكراً. قد يبدو الأمر بسيطاً: تأخذ range، تمر عبر المفاتيح، وتحصل على القيم. لكن الشيطان، كما هو الحال دائماً، في التفاصيل. سؤال "Iterating over all the keys of a map" على Stack Overflow حصد آلاف المشاهدات، وهذا ليس من قبيل الصدفة.

\n\n

اليوم سنحلل أهم الأخطاء عند التكرار على map في Go التي يرتكبها حتى المطورون ذوو الخبرة. ستتعرف لماذا ترتيب الاجتياز هو يانصيب، وكيفية العمل بأمان مع map في goroutines، وماذا تفعل إذا كان map فجأة nil. هيا بنا!

\n\n

الخطأ رقم 1: الاعتماد على الترتيب (أو "أين مفاتيحي؟")

\n\n

أكثر فخ شائع هو الاعتقاد بأن range سيعيد المفاتيح بنفس الترتيب الذي أضفتها به. في Go هذا غير صحيح. ترتيب التكرار على map هو غير حتمي بشكل أساسي.

\n\n

انظر إلى هذا الكود:

\n\n
package 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
\n\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).

\n\n

الخطأ رقم 2: التكرار مع الكتابة المتزامنة (data race)

\n\n

في Go، map غير آمن للوصول المتزامن. إذا كانت إحدى goroutines تقرأ (تكرر) map بينما تكتب أخرى فيها في نفس الوقت، ستحصل على fatal error: concurrent map iteration and map write.

\n\n

مثال على خطأ فادح:

\n\n
package 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
  1. sync.Mutex - الميوتكس الكلاسيكي لمزامنة الوصول:
  2. \n
\n\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                

المدونات

توصيات الكتب