از زمان انتشار Go 1.18، جنریکها زبان را متحول کردهاند و امکان نوشتن کد قابل استفاده مجدد و ایمن از نظر نوع را بدون کدهای تکراری interface{} یا بازتاب فراهم کردهاند. برای توسعهدهندگان متوسط و پیشرفته، تسلط بر جنریکها دیگر اختیاری نیست—بلکه برای نوشتن برنامههای Go ایتماتیک، با عملکرد بالا و قابل نگهداری ضروری است.
این برگه تقلب، ۱۰ الگوی ضروری را که رایجترین موارد استفاده را پوشش میدهند، خلاصه میکند: دستکاری برشها، مدیریت نقشهها و بهرهگیری از محدودیتهای نوع سفارشی. چه در حال ساخت یک پایپلاین پردازش داده باشید و چه یک دستگیرنده API پیچیده، این الگوها به شما کمک میکنند تا کد 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. محدودیتهای سفارشی با انواع اتحادی
میتوانید رابطهای سفارشی را برای محدود کردن پارامترهای نوع به چندین نوع متمایم تعریف کنید. این روش برای توابعی که نیاز به پذیرش همزمان رشتهها و اعداد صحیح دارند، قدرتمند است.
type Number interface {
int | int64 | float32 | float64
}
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
4. فیلتر کردن برشهای جنریک
فیلتر کردن یکی از رایجترین عملیات روی برشها است. یک تابع فیلتر جنریک یک تابع پیشیکات را میپذیرد که یک مقدار بولی را بازگردانده و به منطقهای بسیار قابل استفاده مجدد امکان میدهد.
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
}
// Usage:
// evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })
5. ادغام نقشههای جنریک
ادغام نقشهها به دلیل محدودیتهای کلید با جنریکها چالشبرانگیز است. میتوانید یک تابع جنریک ایجاد کنید که دو نقشه با انواع کلید و مقدار یکسان را ادغام کند.
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. مقادیر متمایز در برشها
برای اطمینان از یکتایی در یک برش، میتوانید از یک نقشه به عنوان یک مجموعه موقت استفاده کنید. این الگو نیاز دارد که پارامتر نوع `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. نگاشت ساختارهای جنریک (نگاشت شیء)
هنگام تبدیل بین نمایشهای مختلف ساختار، جنریکها به شما امکان میدهند فیلدها را بدون بازتاب و به صورت ایمن نگاشت کنید، به شرطی که منطق تبدیل را در خارج تعریف کنید یا از کتابخانههای کمکی استفاده نمایید.
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. گروهبندی برشها بر اساس کلید
گروهبندی عناصر یک برش در سطلها بر اساس یک تابع کلید، یک الگوی کلاسیک برنامهنویسی تابعی است. این روش نیاز دارد که `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
}
// Note: Direct pointer constraints are complex; usually, you constrain the underlying type
// and accept both pointers and values via separate functions or careful design.
نتیجهگیری
جنریکهای Go ابزار قدرتمندی هستند، اما نیاز به کاربرد اندیشمندانه دارند. الگوهای بالا نشان میدهند که چگونه میتوان سناریوهای رایج مانند فیلتر کردن، نگاشت و گروهبندی را در حالی که ایمنی نوع حفظ میشود، مدیریت کرد. به خاطر داشته باشید که جنریکها باید کد را سادهتر کنند، نه پیچیدهتر. با این ده الگو شروع کنید، با محدودیتهای سفارشی آزمایش کنید و به سرعت مزایای آن را در پروژههای Go خود خواهید دید.