۶.۹.۱ ساخت کتابخانههای عمومی و 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 است، اما باید همیشه در پروژههای واقعی تست شوند.