«أين 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، بل ونكتب تنفيذنا البسيط الخاص.
لماذا لم يضيف مبتكري Go Set إلى المكتبة القياسية؟
\n\nيكمن الجواب في البساطة والواقعية للغة. التزم فريق Go (روب بايك، كين تومسون، روبرت غريزيمر) بمبدأ: «إذا كان من الممكن عمل شيء ما ببنية لغة واحدة — فلا تخلق كيانات». في Go، map ليس مجرد قاموس، بل هو حاوية متعددة الاستخدامات. إنه يدعم بالفعل تفرد المفاتيح، وهي الخاصية الأساسية للمجموعة.
فلسفة «سحر أقل — وضوح أكثر»
\n\nعلى عكس Python، حيث set هو نوع منفصل بطرقه الخاصة (.add(), .union())، قرروا في Go عدم تعقيد اللغة. لماذا كتابة بنية منفصلة إذا كان map[T]struct{} يفعل نفس الشيء؟ هذا يوفر مساحة في المكتبة القياسية ويخفض حاجز الدخول للمبتدئين. الشيء الرئيسي هو تعلم كيفية استخدام map بشكل صحيح كـ set.
كيفية محاكاة Set في Go: ثلاث طرق عملية
\n\nدعنا ننتقل إلى التطبيق العملي. إليك كيفية تنفيذ مجموعة باستخدام أدوات Go القياسية فقط.
\n\n1. الأكثر شيوعاً: map[type]struct{}
\n\nهذا هو النهج القياسي في مجتمع Go. لماذا struct{}؟ لأن البنية الفارغة لا تشغل ذاكرة (0 بايت) — مثالية لوضع علامة «وجود» العنصر.
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.
2. نوع مخصص مع طرق
\n\nإذا كنت بحاجة إلى واجهة برمجة تطبيقات نظيفة، قم بتغليف map في بنية:
\n\npackage 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\n3. استخدام bool بدلاً من struct{}
\n\nيستخدم البعض map[T]bool، لكن هذا يستهلك 1 بايت لكل قيمة. في معظم الحالات، الفرق غير ملحوظ، ولكن إذا كنت تكتب خدمة عالية التحميل — فمن الأفضل الالتزام بـ struct{}.