از زمان معرفی جنریکها در Go 1.18، این زبان انعطافپذیری بیسابقهای کسب کرده است بدون آنکه فلسفه اصلی سادگی و عملکرد خود را فدا کند. برای توسعهدهندگان متوسط و پیشرفته، استفاده از جنریکها برای انتزاع کدهای تکراری رایج—مانند ارسال درخواستهای HTTP یا پیادهسازی الگوی Repository—دیگر فقط یک ترفند جالب نیست، بلکه یک بهترین شیوه برای ساخت میکروسرویسهای مقیاسپذیر و قابل نگهداری است.
در این راهنما، ما بررسی خواهیم کرد که چگونه میتوان یک ابزار کلاینت HTTP جنریک ایجاد کرد و الگوی Repository را با استفاده از پارامترهای نوع Go اعمال نمود. این رویکرد به طور قابل توجهی تکرار کد را در پروژه شما کاهش میدهد و در عین حال ایمنی نوع سختگیرانه را حفظ میکند.
ساخت یک کلاینت HTTP جنریک
به طور سنتی، ارسال یک درخواست HTTP در Go شامل تنظیم کلاینت، ایجاد درخواست، مدیریت context و مانشال کردن دستی پاسخ JSON به یک ساختار است. اگر چندین نقطه پایانی (endpoint) دارید، این منطق اغلب تکرار میشود. با معرفی یک تابع جنریک، میتوانیم مرحله تجزیه (decoding) را انتزاع کنیم.
پیادهسازی زیر را در نظر بگیرید. ما تابعی به نام DoRequest تعریف میکنیم که یک نوع جنریک T را میپذیرد. این به کامپایلر اجازه میدهد تا اطمینان حاصل کند که بدنه پاسخ به موفقیت در ساختار خاصی که انتظار دارید تجزیه میشود.
package client
import (
"encoding/json"
"io"
"net/http"
)
// DoRequest یک درخواست HTTP GET انجام داده و پاسخ JSON را به نوع جنریک ارائه شده T تجزیه میکند.
func DoRequest[T any](ctx context.Context, client *http.Client, url string) (*T, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result T
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &result, nil
}
این تابع تکی اکنون میتواند برای دریافت ساختارهای User، ساختارهای Order یا هر نوع دیگر قابل سریالسازی JSON استفاده شود، به شرطی که URL معتبر باشد و روش HTTP برابر با GET باشد. در حالی که یک کلاینت آماده برای تولید باید تلاش مجدد و زمانبندیها (timeouts) را مدیریت کند، این مورد قدرت محدودیتهای نوع جنریک را نشان میدهد.
الگوی Repository جنریک
الگوی Repository به عنوان یک میانجی بین لایههای دامنه و نگاشت داده عمل میکند و یک رابط شبیه به مجموعه (collection-like) برای دسترسی به اشیاء دامنه فراهم میکند. در Go، اگرچه ما کلاسها را نداریم، میتوانیم از رابطها و جنریکها برای ایجاد انتزاع Repository بسیار قابل استفاده مجدد استفاده کنیم.
با تعریف یک رابط جنریک، میتوانیم الزام کنیم که هر ساختاری که منطق کسبوکار ما را پیادهسازی میکند، به عملیات خاص CRUD (ایجاد، خواندن، بهروزرسانی، حذف) پایبند باشد. این موضوع هنگام کار با پایگاههای داده مختلف (مثلاً SQL در مقابل NoSQL) یا شبیهسازی سرویسها در تستها به ویژه مفید است.
package repository
import "context"
// Repository یک رابط جنریک برای عملیات CRUD تعریف میکند.
type Repository[T any, ID comparable] interface {
Get(ctx context.Context, id ID) (*T, error)
Create(ctx context.Context, item *T) error
Delete(ctx context.Context, id ID) error
}
// UserService منطق کسبوکار مربوط به کاربران را مدیریت میکند.
type UserService struct {
repo Repository[User, int]
}
func NewUserService(repo Repository[User, int]) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUserProfile(ctx context.Context, id int) (*User, error) {
// منطق کسبوکار میتواند قبل از دریافت از پایگاه داده اینجا اعمال شود
user, err := s.repo.Get(ctx, id)
if err != nil {
return nil, err
}
// مثال: غنیسازی دادههای کاربر
user.Status = "active"
return user, nil
}
مزایای عملی و ملاحظات
استفاده از جنریکها برای الگوهای کلاینت HTTP و Repository سه مزیت اصلی ارائه میدهد:
- ایمنی نوع: کامپایلر Go انواع ناسازگار را در زمان ساخت به جای زمان اجرا تشخیص میدهد که از باگهای ظریفی که در آنها پاسخ JSON به ساختار اشتباه تجزیه میشود، جلوگیری میکند.
- کد Dry (تکرار کمتر): حذف کدهای تکراری سریالسازی و مدیریت خطا، کدبیس شما را برای خواندن و نگهداری آسانتر میکند.
- قابلیت تست: رابطهای جنریک به شما امکان میدهند تا وابستگیها را به راحتی شبیهسازی (mock) کنید. برای مثال، میتوانید یک
MockRepository[T]ایجاد کنید که دادههای از پیش تعریف شده را برای تستUserServiceبدون دسترسی به پایگاه داده یا API واقعی بازگرداند.
با این حال، توسعهدهندگان باید مراقب باشند که بیش از حد مهندسی نکنند. هر تابعی نیاز به جنریک بودن ندارد. از جنریکها جایی استفاده کنید که الگوی واضحی از پارامتریزه کردن نوع میبینید. برای اسکریپتهای ساده یا توابع ابزار یکبار مصرف، انواع مشخص (concrete) معمولاً خوانایی و عملکرد بهتری دارند.
نتیجهگیری
جنریکها در Go در را به سمت الگوهای طراحی پیچیدهتری باز کردهاند که قبلاً پیادهسازی آنها ناهموار یا طولانی بود. با ترکیب یک کلاینت HTTP جنریک با الگوی Repository، میتوانید سیستمهای بکاندی بسازید که هم انعطافپذیر و هم مستحکم هستند. هنگام بازنویسی پروژههای Go موجود خود، به دنبال فرصتهایی برای استخراج منطق تکراری خاص نوع به انتزاعات جنریک باشید. خود آینده شما و تیمتان از بابت کدبیس تمیزتر و قابل نگهداریتر سپاسگزار خواهند بود.