الطريقة التي تنظم بها كود Go منذ البداية تحدد مدى سهولة النمو لاحقاً. هذا كيف نبني خوادم Go لمنتجات SaaS تبقى قابلة للصيانة مع توسع الفريق والميزات.
الطريقة التي تنظم بها كود Go منذ البداية تحدد مدى سهولة النمو لاحقاً. الهيكل الخاطئ يُفضي إلى حزم ضخمة يصعب اختبارها، وأخطاء استيراد دائري تعيق إعادة الهيكلة، وطبقات خدمة تتراكم فيها منطق لا يجب أن تمتلكه. هذا كيف ننظم خوادم Go لمنتجات SaaS نبنيها في لبنان وعبر منطقة الشرق الأوسط.
أكثر الأخطاء الهيكلية شيوعاً
الخطأ الأول هو تنظيم الكود حسب الدور التقني: حزمة handlers، وحزمة models، وحزمة services، وحزمة repositories. يبدو هذا نظيفاً في البداية. لكن عندما يكون لديك خمس عشرة ميزة، تتحول كل حزمة إلى فضاء مشترك. حزمة الـ handlers تحتوي على معالجات HTTP للمستخدمين والطلبات والمخزون والفواتير والتقارير، كلها مختلطة معاً.
الخطأ الثاني هو العكس: وضع كل شيء لميزة ما في ملف واحد بلا تنظيم داخلي. الملف يكبر إلى 2000 سطر.
تنظيم الحزم حسب النطاق الوظيفي أولاً
الهيكل الذي نستخدمه ينظم الكود حسب النطاق أولاً، مع طبقات تقنية داخل كل نطاق:
internal/
domain/
order/
order.go (الأنواع والمنطق)
repository.go (تعريف الواجهة)
service.go (حالات الاستخدام)
tenant/
inventory/
postgres/
order_repository.go
http/
order_handler.go
config/
كل حزمة نطاق لا تستورد أي حزمة تنفيذية. حزمة postgres تستورد حزم النطاق لتنفيذ واجهاتها. هذا يفرض اتجاه تبعية محدداً.
تعريف الواجهات في موضع الاستهلاك
في Go، الواجهة تُعرَّف حيث تُستهلك، لا حيث تُنفَّذ. هذا اختيار تصميمي مقصود:
// internal/domain/order/repository.go
type Repository interface {
Create(ctx context.Context, o Order) error
GetByID(ctx context.Context, tenantID, orderID uuid.UUID) (*Order, error)
UpdateStatus(ctx context.Context, tenantID, orderID uuid.UUID, status Status) error
}
تنفيذ PostgreSQL في حزمة منفصلة يرضي هذه الواجهة ضمنياً بدون أي تصريح صريح. المترجم يتحقق من ذلك في وقت الترجمة.
طبقة الخدمة: منسقون خفيفون
طبقة الخدمة تمتلك منطق حالة الاستخدام: تسلسل الخطوات اللازمة لإتمام عملية. لا تمتلك قواعد العمل (تلك تعيش في أنواع النطاق)، ولا تمتلك الوصول للبيانات (ذلك يعيش في المستودع):
func (s *Service) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (*Order, error) {
o, err := NewOrder(req.TenantID, req.CustomerID, req.Items)
if err != nil {
return nil, fmt.Errorf("order: build: %w", err)
}
if err := s.inventory.Reserve(ctx, o.TenantID, o.Items); err != nil {
return nil, fmt.Errorf("order: reserve inventory: %w", err)
}
if err := s.orders.Create(ctx, *o); err != nil {
return nil, fmt.Errorf("order: persist: %w", err)
}
return o, nil
}
حقن التبعيات بدون إطار عمل
Go لا تحتاج إطار DI. ربط التبعيات يحدث في main.go باستدعاءات المُنشئ العادية:
orderRepo := postgres.NewOrderRepository(db)
inventoryRepo := postgres.NewInventoryRepository(db)
orderSvc := order.NewService(orderRepo, inventoryRepo)
router := http.NewRouter(http.NewOrderHandler(orderSvc))
هذا مطوَّل لكنه صريح تماماً. يمكنك تتبع أي تبعية من main.go للخارج. لا انعكاس وقت التشغيل، لا ملفات تكوين، لا تسجيل حاوية.
تجنب الاستيراد الدائري
الاستيراد الدائري (الحزمة A تستورد B التي تستورد A) خطأ في الترجمة في Go. الهيكل المبني على النطاق يمنع معظم الاستيراد الدائري بالتصميم: حزم النطاق لا تستورد بعضها مباشرة.
عندما يحتاج النطاق A إلى شيء من النطاق B، يُعرِّف واجهة لما يحتاجه بدلاً من استيراد النوع المادي:
// في service.go لنطاق order
type TenantLookup interface {
Exists(ctx context.Context, tenantID uuid.UUID) (bool, error)
}
الربط في main.go يصل بينهما. لا أي من حزم النطاق يستورد الأخرى.
دروس من بيئات الإنتاج
- نظِّم حسب النطاق، مع طبقات تقنية داخل كل حزمة نطاق.
- عرِّف الواجهات في موضع الاستهلاك، لا في موضع التنفيذ.
- اجعل أساليب الخدمة منسقين خفيفين: تسلسل خطوات، لا قواعد عمل.
- اربط جميع التبعيات في
main.goبمُنشئات عادية. - استخدم مجلد
internal/لفرض الحدود ومنع الاستيرادات الخارجية العرضية.
نبني خوادم Go للـ SaaS للشركات في لبنان وعبر الشرق الأوسط. إذا كنت تبدأ منتجاً جديداً أو تتعامل مع قاعدة كود تجاوزت هيكلها، يمكننا مساعدتك في تصميم بنية تبقى قابلة للصيانة على المدى البعيد. تواصل معنا عبر https://voxire.com/get-a-quote/



