6.9 مثال‌های پیشرفته و نکات ویژه

6.9 مثال‌های پیشرفته و نکات ویژه

۶.۹.۱ ساخت کتابخانه‌های عمومی و abstractionهای حرفه‌ای با ژنریک‌ها #

در پروژه‌های مدرن، معمولاً نیاز به abstraction و لایه‌بندی وجود دارد تا بتوانید کدهای reusable و توسعه‌پذیر بسازید. ژنریک‌ها در Go این کار را بسیار ساده و حرفه‌ای می‌کنند.

مثال: کتابخانه DataStore ژنریک #

type DataStore[T any] interface {
    Get(id string) (T, error)
    Save(id string, data T) error
}

type MemoryStore[T any] struct {
    data map[string]T
}

func NewMemoryStore[T any]() *MemoryStore[T] {
    return &MemoryStore[T]{data: make(map[string]T)}
}

func (m *MemoryStore[T]) Get(id string) (T, error) {
    v, ok := m.data[id]
    if !ok {
        var zero T
        return zero, fmt.Errorf("not found")
    }
    return v, nil
}

func (m *MemoryStore[T]) Save(id string, data T) error {
    m.data[id] = data
    return nil
}

توضیح:
در این مثال یک abstraction برای ذخیره‌سازی داده‌ها پیاده‌سازی شده که می‌تواند برای هر نوع داده‌ای مورد استفاده قرار گیرد (مثلاً User, Order, Product و …). این ساختار با پیاده‌سازی interface ژنریک، قابلیت توسعه و تست بسیار بالایی دارد و به راحتی می‌توانید MemoryStore را با نسخه DatabaseStore یا CacheStore جایگزین کنید.

۶.۹.۲ ترکیب ژنریک با error handling و context #

ترکیب ژنریک با الگوهای حرفه‌ای مثل مدیریت خطا (error handling) و context در Go باعث ایجاد کدهایی ایمن، تمیز و مقیاس‌پذیر می‌شود.

مثال: سرویس ژنریک با Context و Error #

type Service[T any] interface {
    FindByID(ctx context.Context, id int) (T, error)
}

type User struct {
    Name string
}

type UserService struct {
    data map[int]User
}

func (u *UserService) FindByID(ctx context.Context, id int) (User, error) {
    select {
    case <-ctx.Done():
        return User{}, ctx.Err()
    default:
        user, ok := u.data[id]
        if !ok {
            return User{}, fmt.Errorf("not found")
        }
        return user, nil
    }
}

توضیح:
در این الگو، abstraction سرویس به صورت ژنریک تعریف شده و متدها از context و error استفاده می‌کنند. این الگو مناسب سرویس‌های REST, gRPC، کار با پایگاه داده و معماری‌های مدرن است.

۶.۹.۳ نکات بهینه‌سازی و Performance در کد ژنریک #

برای کدهای ژنریک، همواره باید کارایی و بهینه‌سازی را در نظر گرفت، مخصوصاً در ساختارهای داده و توابع پرتکرار.

نکات مهم: #

  • استفاده از constraintهای حداقلی:
    constraintها را تا جای ممکن ساده نگه دارید تا کامپایلر بتواند بیشترین بهینه‌سازی را انجام دهد.

  • اجتناب از reflect و type assertion:
    هرجا می‌توانید منطق را با constraint و متدهای مستقیم حل کنید و از عملیات runtime اضافه بپرهیزید.

  • بنچمارک عملی:
    کدهای ژنریک را مثل سایر کدها با بنچمارک مقایسه کنید، به ویژه اگر در مسیر بحرانی اجرا قرار دارند.

  • استفاده از slices و pre-allocation:
    در ساختارهای داده، اندازه اولیه slice را تعیین کنید تا از افزایش هزینه reallocation جلوگیری شود.

  • پروفایلینگ کد ژنریک:
    با ابزارهایی مثل pprof، عملکرد توابع ژنریک را بررسی کنید تا نقاط bottleneck را شناسایی و رفع کنید.

مثال بنچمارک ساده: #

func BenchmarkMaxInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Max(123, 456)
    }
}

func BenchmarkMaxGeneric(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Max[int](123, 456)
    }
}

توضیح:
این بنچمارک‌ها نشان می‌دهند که در عمل، تفاوت سرعت نسخه ژنریک و نسخه معمولی minimal است، اما باید همیشه در پروژه‌های واقعی تست شوند.