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

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

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

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

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

 1type DataStore[T any] interface {
 2    Get(id string) (T, error)
 3    Save(id string, data T) error
 4}
 5
 6type MemoryStore[T any] struct {
 7    data map[string]T
 8}
 9
10func NewMemoryStore[T any]() *MemoryStore[T] {
11    return &MemoryStore[T]{data: make(map[string]T)}
12}
13
14func (m *MemoryStore[T]) Get(id string) (T, error) {
15    v, ok := m.data[id]
16    if !ok {
17        var zero T
18        return zero, fmt.Errorf("not found")
19    }
20    return v, nil
21}
22
23func (m *MemoryStore[T]) Save(id string, data T) error {
24    m.data[id] = data
25    return nil
26}

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

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

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

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

 1type Service[T any] interface {
 2    FindByID(ctx context.Context, id int) (T, error)
 3}
 4
 5type User struct {
 6    Name string
 7}
 8
 9type UserService struct {
10    data map[int]User
11}
12
13func (u *UserService) FindByID(ctx context.Context, id int) (User, error) {
14    select {
15    case <-ctx.Done():
16        return User{}, ctx.Err()
17    default:
18        user, ok := u.data[id]
19        if !ok {
20            return User{}, fmt.Errorf("not found")
21        }
22        return user, nil
23    }
24}

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

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

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

نکات مهم: #

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

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

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

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

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

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

 1func BenchmarkMaxInt(b *testing.B) {
 2    for i := 0; i < b.N; i++ {
 3        _ = Max(123, 456)
 4    }
 5}
 6
 7func BenchmarkMaxGeneric(b *testing.B) {
 8    for i := 0; i < b.N; i++ {
 9        _ = Max[int](123, 456)
10    }
11}

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