6.3 سینتکس و ساختار ژنریک‌ها در Go

6.3 سینتکس و ساختار ژنریک‌ها در Go

۶.۳.۱ تعریف تابع ژنریک (Generic Functions) #

در Go از نسخه ۱.۱۸، می‌توانید توابعی بنویسید که به‌جای نوع خاص، با نوع پارامتری کار می‌کنند. پارامترهای نوعی (type parameters) در کروشه [] بعد از نام تابع قرار می‌گیرند.

نمونه سینتکس: #

func Swap[T any](a, b T) (T, T) {
    return b, a
}
  • T پارامتر نوعی است که می‌تواند هر نوعی را بپذیرد (در اینجا با constraint any).

  • تابع بالا می‌تواند برای هر نوعی (int، string، ساختار دلخواه و …) فراخوانی شود:

    a, b := Swap[int](1, 2)      // خروجی: 2, 1
    x, y := Swap[string]("a", "b") // خروجی: "b", "a"
    
  • Type Inference: معمولاً Go نوع را به طور خودکار تشخیص می‌دهد و نیازی به ذکر [int] نیست:

    s, t := Swap("hello", "world")
    

با constraint (محدودیت نوع): #

func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

در اینجا فقط انواع مرتب‌شونده (int, float64, string, …) مجاز هستند.

۶.۳.۲ تعریف نوع (Type) ژنریک (Generic Types) #

شما می‌توانید struct، slice، map یا هر نوع داده دلخواه را به صورت ژنریک تعریف کنید تا برای انواع مختلف قابل استفاده باشد.

مثال Struct ژنریک: #

type Box[T any] struct {
    Value T
}
  • اکنون می‌توانید Box را برای هر نوعی استفاده کنید:

    var intBox Box[int]
    intBox.Value = 42
    
    var strBox Box[string]
    strBox.Value = "Go!"
    

مثال عملی – Stack ژنریک: #

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) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    idx := len(s.items) - 1
    item := s.items[idx]
    s.items = s.items[:idx]
    return item, true
}

این ساختار را می‌توانید برای int، string، struct دلخواه و … به کار ببرید.

۶.۳.۳ تعریف اینترفیس ژنریک (Generic Interfaces) #

از Go 1.18+، می‌توانید interfaceهایی با پارامتر نوع بنویسید. این امکان بسیار قدرتمند است و اجازه abstraction و تعریف constraintهای پیچیده را می‌دهد.

نمونه سینتکس: #

type Equaler[T any] interface {
    Equal(T) bool
}
  • هر نوعی که متد Equal(T) bool داشته باشد می‌تواند پیاده‌ساز این اینترفیس باشد.

مثال – Set ژنریک: #

type Set[E any] interface {
    Insert(E)
    Delete(E)
    Has(E) bool
}
  • اکنون می‌توانید انواع مختلف Set برای انواع داده متفاوت پیاده‌سازی کنید.

Constraint ژنریک مبتنی بر متد: #

type Comparer[T any] interface {
    Compare(T) int
}
  • این الگو به شما اجازه می‌دهد فقط با انواعی کار کنید که متد Compare دارند.

نکته کاربردی: تعریف constraintها و abstractionهای پیشرفته با استفاده از generic interface در نسخه‌های جدید Go (به ویژه Go 1.21+ و Go 1.24) بسیار ساده و قدرتمند شده است.

۶.۳.۴ استفاده همزمان از چند پارامتر نوع (Multiple Type Parameters) #

گاهی لازم است تابع یا نوعی بنویسید که با چند نوع مختلف سر و کار دارد.

نمونه سینتکس: #

func CopyMap[K comparable, V any](m map[K]V) map[K]V {
    newMap := make(map[K]V)
    for k, v := range m {
        newMap[k] = v
    }
    return newMap
}
  • اینجا K کلیدهایی است که باید قابل مقایسه باشند (comparable)، و V می‌تواند هر نوعی باشد.

مثال Struct با چند پارامتر نوع: #

type Pair[A, B any] struct {
    First  A
    Second B
}
  • می‌توانید انواع مختلف را جفت کنید:

    p := Pair[int, string]{First: 1, Second: "Go"}
    

۶.۳.۵ معرفی و کاربرد Generic Type Alias (جدید در Go 1.24) #

در نسخه ۱.۲۴ Go، امکان تعریف type alias برای انواع ژنریک اضافه شد که نقش بسیار مهمی در خوانایی، بازاستفاده و ماژولار کردن کد دارد.

۶.۳.۵.۱ تفاوت Type Alias معمولی و ژنریک #

  • تا قبل از Go 1.24، فقط می‌توانستید برای انواع غیرژنریک alias تعریف کنید:

    type MyInt = int
    
  • از Go 1.24، می‌توانید برای انواع ژنریک هم alias بسازید:

    type Box[T any] struct{ Value T }
    type IntBox = Box[int]         // نوع IntBox معادل Box[int] است
    

۶.۳.۵.۲ کاربردهای عملی Generic Type Alias #

  • کاهش تکرار کد و ساده‌تر شدن refactoring

  • تعریف alias برای توابع ژنریک، constraintها، و حتی mapها و channelهای ژنریک:

    type StringMap[V any] = map[string]V
    type UserChan = chan User
    
  • تمیز و خواناتر شدن APIها و لایه abstraction:

    type ConfigMap = map[string]string
    

۶.۳.۵.۳ بهترین شیوه‌های نام‌گذاری و ضدالگوها #

نام‌های گویا و معنادار انتخاب کنید؛

❌ بد:

    type X = Box[int]

✅ خوب:

type UserIDBox = Box[int]
  • از aliasهای تو در تو و بیش از حد بپرهیزید؛
  • فقط برای کدهایی که واقعاً تکرار می‌شوند و نیاز به abstraction دارند استفاده کنید.

۶.۳.۶ حذف Core Type در Go 1.25 و تأثیر آن بر ژنریک‌ها #

در نسخه‌های اولیه ژنریک Go، مفهومی به نام core type وجود داشت که تعیین می‌کرد یک type parameter در زمان compile-time به چه underlying type‌ای ارجاع داده می‌شود. این مفهوم باعث پیچیدگی و محدودیت در برخی عملیات‌ها (مانند index، slice و …) شده بود.

از Go 1.25 به بعد:

  • core type از استاندارد حذف شد و قواعد هر عملیات به صورت واضح‌تر و مستقل بیان شد.
  • خوانایی و سادگی زبان افزایش یافت و پیام‌های خطا شفاف‌تر شدند.
  • حالا فقط کافی است برای عملیات مورد نظر constraint درست تعریف شود (مثلاً اگر با map کار می‌کنید باید comparable باشد).