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