المشكلة قبل Generics
بدون Generics، لازم دالة منفصلة لكل نوع حتى لو المنطق متطابق:
func SumInts(nums []int) int {
var total int
for _, n := range nums {
total += n
}
return total
}
func SumFloats(nums []float64) float64 {
var total float64
for _, n := range nums {
total += n
}
return total
}
نفس الكود، مكرّر لكل نوع. هذا بالضبط ما تحلّه Generics.
دالة عامة بمعامل نوع
معامل النوع يُكتب بين قوسين مربّعين [T ...] قبل معاملات الدالة العادية:
func Sum[T int | float64](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
Sum([]int{1, 2, 3}) // 6
Sum([]float64{1.5, 2.5}) // 4
T هنا معامل نوع (type parameter)، و int | float64 قيد (constraint) يحدّد الأنواع المسموحة. غالبًا لا تحتاج كتابة النوع صراحةً — المترجم يستنتجه من الوسيطة.
القيد المدمج comparable
comparable قيد جاهز يسمح بأي نوع تدعم قيمه == و != — مطلوب مثلًا لمفاتيح الخرائط:
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
Contains([]string{"أ", "ب", "ج"}, "ب") // true
تعريف قيد مخصّص
القيد نفسه واجهة (interface) تجمع الأنواع المسموحة باستخدام |:
type Number interface {
int | int64 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
هذا يفيد لما يتكرّر نفس القيد بأكثر من دالة بدل إعادة كتابة الاتحاد كل مرّة.
أنواع عامة (Generic Types)
الـ structs والأنواع الأخرى تقبل معاملات نوع أيضًا:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
var s Stack[int]
s.Push(10)
s.Push(20)
val, ok := s.Pop() // 20, true
any هنا (اختصار interface{}) يعني «أي نوع بلا قيود» — Stack تعمل مع الأعداد والنصوص وأي نوع آخر بنفس الكود.
💡 استخدم Generics لما تكرّر نفس المنطق لأنواع مختلفة فعلًا — لا تعقّد كودك البسيط بها بلا داعٍ. أغلب كود Go اليومي لا يحتاجها.
الدوال العامة على المستقبِل (Generic Methods) — منذ Go 1.27
كل الأمثلة السابقة كانت دوال عامة (قوس مربّع بعد اسم الدالة) أو أنواع عامة بمُستقبِل عادي — لكن حتى Go 1.26 لم يكن مسموحًا لمُستقبِل method نفسه أن يعلن معامل نوع خاصًّا به منفصلًا عن النوع الذي ينتمي إليه. منذ Go 1.27، صار ذلك ممكنًا: تعريف method يقدر يعلن معاملات نوع خاصّة به، بالضبط كما تفعل دالة عادية بمستوى الحزمة:
type Box struct {
label string
}
// معامل النوع T خاص بهذا الـ method فقط -- مش بنوع Box نفسه
// يُكتب بين اسم الـ method وقائمة معاملاته، تمامًا كما بدالة عامة عادية
func (b Box) Map[T any](items []T, transform func(T) T) []T {
result := make([]T, len(items))
for i, item := range items {
result[i] = transform(item)
}
return result
}
b := Box{label: "أرقام"}
b.Map([]int{1, 2, 3}, func(n int) int { return n * 2 }) // [2 4 6]
الفرق عن نوع عام (مثل Stack[T] بالأعلى): هناك T جزء من تعريف النوع نفسه، فكل استخدام لـ Stack[int] يبقى مرتبطًا بـ int طوال عمر القيمة. هنا T خاص باستدعاء الـ method نفسه فقط — كل استدعاء لـ b.Map يقدر يمرّر نوعًا مختلفًا، وBox نفسه يبقى نوعًا عاديًا غير عام.
⚠️ قيد مهم: methods الواجهات (interfaces) لا يمكن أن تُعلن معاملات نوع خاصّة بها، ولا يمكن لـ generic method (بمعناها هذا) أن تحقّق method مطلوبة بواجهة — القيد موجود لأن التوقيع يختلف حسب T المُمرَّر، فلا يوجد توقيع ثابت واحد تحقّقه الواجهة.
🎯 التالي: حزمة context والإلغاء.