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

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

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

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

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

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

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

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

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

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

1func Max[T cmp.Ordered](a, b T) T {
2    if a > b {
3        return a
4    }
5    return b
6}

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

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

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

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

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

    1var intBox Box[int]
    2intBox.Value = 42
    3
    4var strBox Box[string]
    5strBox.Value = "Go!"
    

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

 1type Stack[T any] struct {
 2    items []T
 3}
 4
 5func (s *Stack[T]) Push(item T) {
 6    s.items = append(s.items, item)
 7}
 8
 9func (s *Stack[T]) Pop() (T, bool) {
10    if len(s.items) == 0 {
11        var zero T
12        return zero, false
13    }
14    idx := len(s.items) - 1
15    item := s.items[idx]
16    s.items = s.items[:idx]
17    return item, true
18}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    1type ConfigMap = map[string]string
    

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

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

❌ بد:

1    type X = Box[int]

✅ خوب:

1type 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 باشد).