مقدمة: ما هي Generics ولماذا ظهرت في Go؟
لفترة طويلة، استغنى مجتمع Go (Golang) عن الجينيريكس (البرمجة العامة). استخدم المطورون الواجهات، و interface{} الفارغة، وتوليد الكود لتجاوز قيود الكتابة الصارمة. في الإصدار Go 1.18 (فبراير 2022)، تمت إضافة الجينيريكس أخيرًا إلى اللغة — وهي أداة قوية تتيح كتابة دوال وهياكل تعمل مع أنواع بيانات مختلفة دون فقدان أمان الأنواع.
في هذه المقالة، سنشرح لماذا نحتاج إلى Generics في Go، وكيف تعمل، ونتعلم كيفية تطبيقها عمليًا. المادة موجهة للمطورين المبتدئين الذين هم على دراية بأساسيات Go: المتغيرات والدوال والشرائح.
لماذا نحتاج إلى الجينيريكس في Go؟
تخيل أنك بحاجة لكتابة دالة تجد القيمة الدنيا في شريحة من الأرقام. بدون الجينيريكس، سيتعين عليك كتابة دوال منفصلة لـ int و float64 وأنواع أخرى:
// بدون جينيريكس — ثلاث دوال متطابقةfunc MinInt(a, b int) int { if a < b { return a } return b}
func MinFloat64(a, b float64) float64 { if a < b { return a } return b}يؤدي هذا إلى تكرار الكود. في السابق، كان يتم حل هذه المشكلة عبر interface{} وتحويل الأنواع، لكن هذا الأسلوب كان يفقد الأمان في مرحلة الترجمة:
func Min(a, b interface{}) interface{} { // المقارنة والتحويل — من السهل ارتكاب الأخطاء return a}الجينيريكس تحل ثلاث مهام رئيسية:
- أمان الأنواع — يتم اكتشاف أخطاء الأنواع في مرحلة الترجمة.
- القضاء على التكرار — دالة واحدة تعمل مع أي أنواع.
- قابلية قراءة الكود — تحديد صريح للأنواع المدعومة.
أساسيات الصياغة: type parameters و constraints
الجينيريكس في Go مبنية على مفهومين: معاملات النوع (type parameters) و القيود (constraints).
معاملات النوع
تُحدد معاملات النوع بين قوسين مربعين بعد اسم الدالة أو النوع. حسب الاتفاق، غالبًا ما يُستخدم الحرف T (من Type)، ولكن يمكن استخدام أي أسماء:
// دالة بمعامل نوع واحد Tfunc Print[T any](value T) { fmt.Println(value)}
// الاستخدامPrint[int](42) // نحدد النوع صراحةPrint("Hello") // المترجم يستنتج النوع تلقائيًاالكلمة المفتاحية any هي اسم مستعار للواجهة الفارغة interface{}. تعني أن الدالة تقبل أي نوع.
القيود (constraints)
تحدد القيود الأنواع التي يمكن استخدامها مع الجينيريك. على سبيل المثال، لمقارنة الأرقام، يجب أن يدعم النوع العوامل < و >. القيود المضمنة للأنواع الرقمية موجودة في الحزمة golang.org/x/exp/constraints:
import "golang.org/x/exp/constraints"
// قيد — الأعداد الصحيحة فقطfunc Sum[T constraints.Integer](numbers []T) T { var total T for _, n := range numbers { total += n } return total}يمكنك أيضًا إنشاء قيودك الخاصة عبر الواجهات:
// قيد مخصص: يجب أن يحتوي النوع على طريقة String()type Stringer interface { String() string}
func Describe[T Stringer](value T) string { return "Value: " + value.String()}أمثلة عملية لاستخدام Generics
لننظر إلى عدة سيناريوهات حقيقية حيث تبسط الجينيريكس الكود بشكل كبير.
1. دالة بحث عامة في شريحة
// تجد فهرس عنصر في شريحة. -1 إذا لم يتم العثور عليه.func Index[T comparable](slice []T, target T