Go Programming

دليل جوي لـ Go Generics: 10 أنماط برمجية عملية للمصفوفات والقوائم والقيود المخصصة

منذ إصدار Go 1.18، غيّرت الـ Generics من لغة البرمجة، مما يتيح كتابة كود آمن من حيث الأنواع وقابل لإعادة الاستخدام دون الحاجة إلى boilerplate الخاص بـ interface{} أو الانعكاس (reflection). للمطورين المتوسطين والمتقدمين، لم يعد إتقان الـ Generics خياراً بل ضرورة لكتابة تطبيقات Go سليمة وأدائية وقابلة للصيانة.

يُلخّص هذا الدليل 10 أنماط أساسية تغطي أكثر حالات الاستخدام شيوعاً: معالجة القوائم (Slices)، والتعامل مع المصفوفات (Maps)، والاستفادة من قيود الأنواع المخصصة. سواء كنت تبني خط أنابيب لمعالجة البيانات أو معالج واجهة برمجة تطبيقات معقد، ستساعدك هذه الأنماط في كتابة كود Go أنظف.

1. قيد الهوية: معاملات النوع الأساسية

أبسط شكل لدالة عامة يستخدم بادئة `any` (والتي تعادل `interface{}`) أو معامل نوع مقيد بمجموعة محددة من الأنواع. هذا مثالي عندما تحتاج إلى إرجاع قيمة من نفس نوع الإدخال.

func firstElement[T any](s []T) (T, bool) {
    var zero T
    if len(s) == 0 {
        return zero, false
    }
    return s[0], true
}

2. القيود الرقمية للعمليات الرياضية

توفر المكتبة القياسية لـ Go قيوداً مدمجة مثل `~int` أو `~float64`. يشير المائل (~) إلى أن النوع الأساسي يجب أن يتطابق مع نوع عدد صحيح أو كسري، مما يتيح لك العمل مع أنواع الأعداد الصحيحة المخصصة أيضاً.

func SumNumbers[N int | float64](nums []N) N {
    var sum N
    for _, n := range nums {
        sum += n
    }
    return sum
}

3. قيود مخصصة باستخدام أنواع الاتحاد (Union Types)

يمكنك تعريف واجهات مخصصة لتقييد معاملات النوع على أنواع متعددة ومتميزة. هذا قوي للدوال التي تحتاج إلى قبول كل من السلاسل والأعداد الصحيحة.

type Number interface {
    int | int64 | float32 | float64
}

func Max[T Number](a, b T) T {
    if a > b {
        return a
    }
    return b
}

4. تصفية القوائم العامة (Generic Slice Filtering)

يُعد التصفية واحدة من أكثر العمليات شيوعاً على القوائم. تقبل دالة التصفية العامة دالة شرطية (predicate) تُرجع قيمة منطقية، مما يسمح بمنطق قابل لإعادة الاستخدام بشكل كبير.

func Filter[T any](items []T, predicate func(T) bool) []T {
    var result []T
    for _, item := range items {
        if predicate(item) {
            result = append(result, item)
        }
    }
    return result
}

// الاستخدام:
// evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })

5. دمج المصفوفات العامة (Generic Map Merging)

يُعد دمج المصفوفات أمراً محيراً مع الـ Generics بسبب قيود المفاتيح. يمكنك إنشاء دالة عامة تدمج مصفوفتين لهما نفس نوع المفتاح ونوع القيمة.

func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
    merged := make(map[K]V)
    for _, m := range maps {
        for k, v := range m {
            merged[k] = v
        }
    }
    return merged
}

6. القيم المميزة في القوائم

لضمان التفرد في القائمة، يمكنك الاستفادة من مصفوفة كمجموعة مؤقتة (set). يتطلب هذا النمط أن يكون معامل النوع `T` قابلاً للمقارنة.

func Distinct[T comparable](items []T) []T {
    seen := make(map[T]bool)
    var result []T
    for _, item := range items {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

7. تعيين الهياكل العامة (Object Mapping)

عند التحويل بين تمثيلات هيكلية مختلفة، تتيح لك الـ Generics تعيين الحقول بأمان دون استخدام الانعكاس (reflection)، شريطة أن تحدد منطق التحويل خارجياً أو تستخدم مكتبات مساعدة.

func MapStructs[T, U any](items []T, fn func(T) U) []U {
    result := make([]U, len(items))
    for i, item := range items {
        result[i] = fn(item)
    }
    return result
}

8. الوصول الآمن إلى المصفوفات مع قيم افتراضية

غالباً ما يتطلب الوصول إلى مفاتيح المصفوفة التحقق من الوجود لتجنب القيم الصفرية. يمكن لدالة عامة إرجاع قيمة افتراضية إذا كان المفتاح مفقوداً.

func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultVal V) V {
    if val, ok := m[key]; ok {
        return val
    }
    return defaultVal
}

9. تجميع القوائم حسب المفتاح

يُعد تجميع عناصر القائمة في دلاء (buckets) بناءً على دالة مفتاح نمطاً كلاسيكياً في البرمجة الوظيفية. يتطلب هذا أن يكون `K` قابلاً للمقارنة.

func GroupBy[K comparable, V any](items []V, keyFunc func(V) K) map[K][]V {
    groups := make(map[K][]V)
    for _, item := range items {
        key := keyFunc(item)
        groups[key] = append(groups[key], item)
    }
    return groups
}

10. أنواع المؤشرات المقيدة

أحياناً تحتاج إلى العمل مع مؤشرات إلى الهياكل. يمكنك تقييد معاملات النوع للمؤشرات باستخدام الواجهات أو تركيب معين، على الرغم من أن ذلك أقل شيوعاً من أنواع القيم.

type PointerToStruct interface {
    *MyStruct
}

// ملاحظة: قيود المؤشرات المباشرة معقدة؛ عادةً، تقيد النوع الأساسي
// وتقبل كل من المؤشرات والقيم عبر دوال منفصلة أو تصميم دقيق.

الخاتمة

تُعد الـ Generics في Go أداة قوية، لكنها تتطلب تطبيقاً مدروساً. توضح الأنماط أعلاه كيفية التعامل مع السيناريوهات الشائعة مثل التصفية، والتعيين، والتجميع مع الحفاظ على أمان النوع. تذكر أن الـ Generics يجب أن تبسط الكود، لا أن تعقده. ابدأ بهذه الأنماط العشرة، وتجرب القيود المخصصة، وستلاحظ بسرعة الفوائد في مشاريع Go الخاصة بك.

Share: