مقارنة الشرائح في Go: كيف لا تفقد الأداء ولا تصاب بالجنون
\n\nتخيل الموقف: أنت تكتب خدمة عالية التحميل بلغة Go، وتحتاج إلى مقارنة شريحتين من البايت أو السلاسل النصية. قد يبدو الأمر بسيطًا، أليس كذلك؟ لكن الشيطان، كما هو الحال دائمًا، يكمن في التفاصيل. على Stack Overflow، جمع سؤال "Checking the equality of two slices" آلاف المشاهدات - وهذا ليس من قبيل الصدفة. في هذه المقالة، سنقوم بتحليل المخاطر الخفية التي تنتظر المطور، وكيفية كتابة مقارنة صحيحة، والأهم من ذلك، كيفية القيام بذلك بأسرع ما يمكن.
\n\nلن نستخدم تعريفات أكاديمية مملة. بدلاً من ذلك - أمثلة حية، واختبارات أداء، ونصائح عملية يمكنك تطبيقها اليوم. هيا بنا!
\n\nلماذا المقارنة البسيطة ليست دائمًا الصحيحة؟
\n\nفي Go، الشريحة (slice) ليست مصفوفة. إنها واصف يحتوي على مؤشر للمصفوفة الأساسية، والطول، والسعة. لذلك، عامل المقارنة == لا يعمل مع الشرائح. محاولة كتابة slice1 == slice2 ستؤدي إلى خطأ في الترجمة:
invalid operation: slice1 == slice2 (slice can only be compared to nil)\n\nهذا ليس خطأ، بل ميزة. مطورو Go منعوا عمدًا المقارنة المباشرة للشرائح، لأن ذلك قد يؤدي إلى عواقب غير واضحة. على سبيل المثال، قد تشير شريحتان إلى نفس المصفوفة الأساسية، ولكن بطول مختلف. أو العكس - مصفوفات مختلفة، ولكن بيانات متطابقة.
\n\nإذن، ماذا نفعل؟ هناك عدة طرق، وسننظر في كل منها.
\n\nالطريقة 1: حلقة يدوية (أساسية ولكن موثوقة)
\n\nالطريقة الأكثر وضوحًا هي كتابة دالة تمر عبر جميع العناصر وتقارنها واحدًا تلو الآخر. وهذا يمنحك تحكمًا كاملاً في العملية.
\n\nfunc EqualSlices(a, b []int) bool {\n if len(a) != len(b) {\n return false\n }\n for i := range a {\n if a[i] != b[i] {\n return false\n }\n }\n return true\n}\n\nالإيجابيات: الشفافية، عدم وجود تبعيات، إمكانية التخصيص (مثل المقارنة مع تسامح للأعداد العشرية).
\nالسلبيات: تحتاج إلى كتابة دالة لكل نوع بيانات.
الطريقة 2: reflect.DeepEqual (شامل ولكن بطيء)
\n\nفي المكتبة القياسية، توجد دالة reflect.DeepEqual التي يمكنها مقارنة أي هياكل بيانات - الشرائح، الخرائط، الهياكل. تبدو كالتالي:
import "reflect"\n\nfunc main() {\n a := []int{1, 2, 3}\n b := []int{1, 2, 3}\n fmt.Println(reflect.DeepEqual(a, b)) // true\n}\n\nقد يبدو هذا مثاليًا. ولكن هناك فارق بسيط: reflect.DeepEqual تستخدم الانعكاس (reflection)، وهذا دائمًا مكلف. في الشرائح الصغيرة، الفرق غير ملحوظ، ولكن إذا كان لديك آلاف العناصر - سينخفض الأداء بشكل كبير.
هام: reflect.DeepEqual تعتبر أيضًا أن nil والشريحة الفارغة مختلفتان! قد يكون هذا مفاجئًا:
var a []int = nil\nb := []int{}\nfmt.Println(reflect.DeepEqual(a, b)) // false!\n\nالطريقة 3: bytes.Equal لشرائح البايت (الأمثل)
\n\nإذا كنت تعمل مع []byte، فإن الخيار الأفضل هو دالة bytes.Equal من المكتبة القياسية. إنها مكتوبة بلغة التجميع ومحسّنة إلى أقصى حد:
import "bytes"\n\nfunc main() {\n a := []byte("hello")\n b := []byte("hello")\n fmt.Println(bytes.Equal(a, b)) // true\n}\n\nهذه هي أسرع طريقة لشرائح البايت. داخليًا، تستخدم memcmp على مستوى C، مما يعطي زيادة هائلة في الأداء.
الطريقة 4: المقارنة باستخدام crypto/subtle (للتشفير)
\n\nتستحق حزمة crypto/subtle ذكرًا خاصًا. وظيفتها ConstantTimeCompare مصممة لمقارنة البيانات السرية (كلمات المرور، الرموز المميزة) دون تسريب الوقت. المقارنة العادية قد تكون عرضة لهجمات التوقيت:
import "crypto/subtle"\n\nfunc