6.5 مثال‌ها و کاربردهای عملی ژنریک‌ها

6.5 مثال‌ها و کاربردهای عملی ژنریک‌ها

۶.۵.۱ توابع ژنریک متداول (مانند Min, Max, Map, Filter) #

تابع Min و Max #

 1import "cmp" // از Go 1.21+
 2
 3func Min[T cmp.Ordered](a, b T) T {
 4    if a < b {
 5        return a
 6    }
 7    return b
 8}
 9
10func Max[T cmp.Ordered](a, b T) T {
11    if a > b {
12        return a
13    }
14    return b
15}

توضیح:
این دو تابع ژنریک به شما اجازه می‌دهند با هر نوع داده‌ای که قابل مقایسه با عملگرهای بزرگ‌تر/کوچک‌تر باشد (مثل int، float64، string و…) بیشینه یا کمینه دو مقدار را به دست آورید. پارامتر نوعی T باید قید cmp.Ordered را داشته باشد تا عملیات مقایسه مجاز باشد. این ساختار به جای نوشتن نسخه‌های تکراری برای هر نوع داده، یک تابع عمومی و امن ایجاد می‌کند.

تابع Map (اعمال تابع روی عناصر یک لیست) #

1func Map[T any, R any](input []T, fn func(T) R) []R {
2    result := make([]R, len(input))
3    for i, v := range input {
4        result[i] = fn(v)
5    }
6    return result
7}

توضیح:
این تابع یک لیست (input) را می‌گیرد و یک تابع (fn) را روی هر عنصر آن اجرا می‌کند و خروجی‌های تابع را به عنوان لیست جدید بازمی‌گرداند. نوع ورودی (T) و خروجی (R) کاملاً ژنریک هستند و می‌توانید هر تبدیل یا پردازشی را با این الگو روی لیست‌های خود انجام دهید، مثلاً مربع اعداد، تبدیل عدد به رشته و غیره.

تابع Filter (فیلتر کردن عناصر یک لیست) #

1func Filter[T any](input []T, pred func(T) bool) []T {
2    var result []T
3    for _, v := range input {
4        if pred(v) {
5            result = append(result, v)
6        }
7    }
8    return result
9}

توضیح:
تابع Filter یک لیست و یک تابع شرطی (predicate) می‌گیرد و تنها عناصر لیست را که شرط روی آن‌ها برقرار است، انتخاب و در یک لیست جدید بازمی‌گرداند. این کار باعث می‌شود بدون تکرار کد برای هر نوع داده، فیلترهای قدرتمند و ایمن داشته باشید (مثلاً استخراج فقط اعداد زوج یا رشته‌هایی با طول خاص).

۶.۵.۲ ساختارهای داده ژنریک (Stack، Queue، List و …) #

Stack ژنریک #

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

توضیح:
این کد یک ساختار داده پشته (Stack) را به صورت ژنریک پیاده‌سازی می‌کند؛ یعنی می‌توانید هر نوع داده‌ای را در پشته ذخیره کنید. متد Push یک مقدار جدید به انتهای پشته اضافه می‌کند و Pop مقدار آخر را حذف و بازمی‌گرداند. اگر پشته خالی باشد، مقدار صفر نوع داده (zero value) برگردانده می‌شود. این پیاده‌سازی قابلیت استفاده برای int، string یا حتی structهای پیچیده را دارد.

Queue ژنریک #

 1type Queue[T any] struct {
 2    data []T
 3}
 4
 5func (q *Queue[T]) Enqueue(val T) {
 6    q.data = append(q.data, val)
 7}
 8
 9func (q *Queue[T]) Dequeue() (T, bool) {
10    if len(q.data) == 0 {
11        var zero T
12        return zero, false
13    }
14    val := q.data[0]
15    q.data = q.data[1:]
16    return val, true
17}

توضیح:
کد بالا یک صف (Queue) ژنریک را پیاده‌سازی می‌کند که برای هر نوع داده‌ای قابل استفاده است. متد Enqueue عنصر جدیدی را به انتهای صف اضافه می‌کند و Dequeue عنصر ابتدای صف را حذف و بازمی‌گرداند. اگر صف خالی باشد، مقدار صفر نوع داده برگردانده می‌شود. این الگو برای مدیریت صف درخواست‌ها یا پیام‌ها با هر نوع داده‌ای بسیار کاربردی است.

List ژنریک #

 1type List[T any] struct {
 2    items []T
 3}
 4
 5func (l *List[T]) Add(val T) {
 6    l.items = append(l.items, val)
 7}
 8
 9func (l *List[T]) Get(index int) (T, bool) {
10    if index < 0 || index >= len(l.items) {
11        var zero T
12        return zero, false
13    }
14    return l.items[index], true
15}

توضیح:
این ساختار یک لیست ساده ژنریک است که می‌توانید هر نوع داده‌ای را به آن اضافه یا با اندیس بازیابی کنید. متد Add برای افزودن و Get برای دریافت مقدار در اندیس دلخواه (همراه با بررسی بازه ایمن) استفاده می‌شود. این ساختار می‌تواند پایه ساخت کلکسیون‌ها و آرایه‌های سفارشی در پروژه‌های بزرگ‌تر باشد.

۶.۵.۳ ترکیب ژنریک با سایر ویژگی‌های Go (کانال‌ها، مپ‌ها و اینترفیس‌ها) #

Channel ژنریک #

1type Chan[T any] chan T
2
3func Producer[T any](out Chan[T], vals ...T) {
4    for _, v := range vals {
5        out <- v
6    }
7    close(out)
8}

توضیح:
در این مثال، نوع کانال (Channel) به صورت ژنریک تعریف شده است، یعنی می‌توانید کانال ارسال/دریافت داده برای هر نوعی بسازید. تابع Producer داده‌های ورودی را به کانال می‌فرستد و در پایان آن را می‌بندد. این الگو برای پردازش موازی و همزمان داده‌ها در معماری‌های concurrent و pipeline بسیار مناسب است.

Map ژنریک با constraint #

1func Keys[K comparable, V any](m map[K]V) []K {
2    keys := make([]K, 0, len(m))
3    for k := range m {
4        keys = append(keys, k)
5    }
6    return keys
7}

توضیح:
تابع Keys یک map را می‌گیرد و لیستی از کلیدهای آن را بازمی‌گرداند. نوع کلید باید قابل مقایسه باشد (comparable)، چون mapهای Go فقط با کلیدهای قابل مقایسه کار می‌کنند. این تابع برای استخراج سریع و type-safe کلیدهای هر map بسیار مفید است.

اینترفیس ژنریک و abstraction #

 1type Repository[T any] interface {
 2    FindByID(id int) (T, error)
 3    Save(entity T) error
 4}
 5
 6type User struct{ Name string }
 7
 8type UserRepo struct{ data map[int]User }
 9
10func (r *UserRepo) FindByID(id int) (User, error) {
11    u, ok := r.data[id]
12    if !ok {
13        return User{}, errors.New("not found")
14    }
15    return u, nil
16}
17func (r *UserRepo) Save(entity User) error {
18    r.data[len(r.data)] = entity
19    return nil
20}

توضیح:
در این مثال، یک اینترفیس ژنریک برای مخزن داده (Repository) تعریف شده است که می‌تواند برای هر نوع داده (مثلاً User) پیاده‌سازی شود. متدهای FindByID و Save عملیات بازیابی و ذخیره را type-safe انجام می‌دهند. این الگو پایه معماری clean و قابل توسعه برای لایه داده در پروژه‌های تولیدی است.

۶.۵.۴ نمونه‌های تولیدی و پروژه‌ای (از کدهای واقعی و کاربردی) #

سرویس کش ژنریک #

 1type Cache[K comparable, V any] struct {
 2    data map[K]V
 3}
 4
 5func NewCache[K comparable, V any]() *Cache[K, V] {
 6    return &Cache[K, V]{data: make(map[K]V)}
 7}
 8
 9func (c *Cache[K, V]) Set(key K, value V) {
10    c.data[key] = value
11}
12
13func (c *Cache[K, V]) Get(key K) (V, bool) {
14    v, ok := c.data[key]
15    return v, ok
16}

توضیح:
در اینجا یک سرویس کش (Cache) به صورت ژنریک پیاده‌سازی شده که برای هر نوع کلید (comparable) و هر نوع مقدار قابل استفاده است. با استفاده از این ساختار می‌توانید بدون تکرار کد برای انواع مختلف داده، کش‌های بهینه و امن بسازید که در پروژه‌های واقعی (مثلاً کش کاربر، تنظیمات یا داده‌های session) بسیار کاربردی است.

Pipeline ژنریک برای پردازش داده‌ها #

1func Pipeline[T any](data []T, stages ...func([]T) []T) []T {
2    for _, stage := range stages {
3        data = stage(data)
4    }
5    return data
6}

توضیح:
تابع Pipeline به شما امکان می‌دهد زنجیره‌ای از مراحل پردازش (stages) را روی لیست داده اجرا کنید. هر مرحله یک تابع است که لیست را می‌گیرد و خروجی پردازش را بازمی‌گرداند. این الگو برای پردازش داده‌های بزرگ، تحلیل داده یا پیاده‌سازی الگوهای data pipeline در سیستم‌های تولیدی بسیار ارزشمند است.