لا يوجد Set في Go: لماذا وكيف تعيش بدون مجموعة مدمجة

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

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

ابدأ الدورة

«أين Set الخاص بي؟» — صرخة روح مطور Go

\n\n

إذا أتيت إلى Go من Python أو JavaScript أو Java، فستسأل عاجلاً أم آجلاً: «أين، بحق الجحيم، Set المفضل لدي؟» — وهذا أمر طبيعي. على Stack Overflow، جمع موضوع «Golang why don't we have a set datastructure» آلاف المشاهدات وعشرات المناقشات الساخنة. اعتاد المطورون على المجموعة المريحة ذات العناصر الفريدة، بينما يقدم Go فقط المصفوفات (arrays) والشرائح (slices) والخرائط (maps). في هذه المقالة، سنحلل فلسفة مبتكري Go، ونتعلم محاكاة Set، بل ونكتب تنفيذنا البسيط الخاص.

\n\n

لماذا لم يضيف مبتكري Go Set إلى المكتبة القياسية؟

\n\n

يكمن الجواب في البساطة والواقعية للغة. التزم فريق Go (روب بايك، كين تومسون، روبرت غريزيمر) بمبدأ: «إذا كان من الممكن عمل شيء ما ببنية لغة واحدة — فلا تخلق كيانات». في Go، map ليس مجرد قاموس، بل هو حاوية متعددة الاستخدامات. إنه يدعم بالفعل تفرد المفاتيح، وهي الخاصية الأساسية للمجموعة.

\n\n

فلسفة «سحر أقل — وضوح أكثر»

\n\n

على عكس Python، حيث set هو نوع منفصل بطرقه الخاصة (.add(), .union())، قرروا في Go عدم تعقيد اللغة. لماذا كتابة بنية منفصلة إذا كان map[T]struct{} يفعل نفس الشيء؟ هذا يوفر مساحة في المكتبة القياسية ويخفض حاجز الدخول للمبتدئين. الشيء الرئيسي هو تعلم كيفية استخدام map بشكل صحيح كـ set.

\n\n

كيفية محاكاة Set في Go: ثلاث طرق عملية

\n\n

دعنا ننتقل إلى التطبيق العملي. إليك كيفية تنفيذ مجموعة باستخدام أدوات Go القياسية فقط.

\n\n

1. الأكثر شيوعاً: map[type]struct{}

\n\n

هذا هو النهج القياسي في مجتمع Go. لماذا struct{}؟ لأن البنية الفارغة لا تشغل ذاكرة (0 بايت) — مثالية لوضع علامة «وجود» العنصر.

\n\n
package main\n\nimport "fmt"\n\nfunc main() {\n    // إنشاء مجموعة من الأعداد الصحيحة\n    set := make(map[int]struct{})\n\n    // إضافة عناصر\n    set[1] = struct{}{}\n    set[2] = struct{}{}\n    set[3] = struct{}{}\n    set[1] = struct{}{} // يتم تجاهل المكرر\n\n    // التحقق من الوجود\n    if _, exists := set[2]; exists {\n        fmt.Println("2 موجود في المجموعة")\n    }\n\n    // الحذف\n    delete(set, 3)\n\n    // التكرار\n    for key := range set {\n        fmt.Println(key)\n    }\n}
\n\n

الإيجابيات: عدم تخصيص ذاكرة للقيم، البساطة. السلبيات: لا توجد طرق .Add() أو .Contains() — كل شيء يتم من خلال صيغة map.

\n\n

2. نوع مخصص مع طرق

\n\n

إذا كنت بحاجة إلى واجهة برمجة تطبيقات نظيفة، قم بتغليف map في بنية:

\n\n
package main\n\nimport "fmt"\n\n// Set — مجموعة عامة (Go 1.18+)\ntype Set[T comparable] struct {\n    items map[T]struct{}\n}\n\n// NewSet ينشئ Set جديد\nfunc NewSet[T comparable]() *Set[T] {\n    return &Set[T]{items: make(map[T]struct{})}\n}\n\n// Add يضيف عنصراً\nfunc (s *Set[T]) Add(item T) {\n    s.items[item] = struct{}{}\n}\n\n// Contains يتحقق من الوجود\nfunc (s *Set[T]) Contains(item T) bool {\n    _, ok := s.items[item]\n    return ok\n}\n\n// Size يعيد الحجم\nfunc (s *Set[T]) Size() int {\n    return len(s.items)\n}\n\nfunc main() {\n    intSet := NewSet[int]()\n    intSet.Add(10)\n    intSet.Add(20)\n    fmt.Println(intSet.Contains(10)) // true\n    fmt.Println(intSet.Size())       // 2\n}
\n\n

هذا يشبه بالفعل بنية بيانات set كاملة في Go. ظهرت الأدوية (generics) في الإصدار 1.18، لذا يعمل الكود على المشاريع الحديثة.

\n\n

3. استخدام bool بدلاً من struct{}

\n\n

يستخدم البعض map[T]bool، لكن هذا يستهلك 1 بايت لكل قيمة. في معظم الحالات، الفرق غير ملحوظ، ولكن إذا كنت تكتب خدمة عالية التحميل — فمن الأفضل الالتزام بـ struct{}.

\n\

المدونات

توصيات الكتب