Go: بنية مشروع Go

آخر تحديث: 2026-08-26

لا تفرض لغة «Go» هيكلًا محددًا للمشروع من خلال إطار عمل ما — لكن هذا يعني أنه يتعين عليك تصميمه بنفسك. فالبنية المدروسة جيدًا تحافظ على وضوح مشروعك عندما يبلغ طوله 1,000 سطر، وتجعله قابلاً للإدارة حتى عندما يصل إلى 100,000 سطر.

عندما تتولى مسؤولية مشروع يكون فيه كل الكود محشورًا في main.go، فإن الخطوة الأولى في إعادة هيكلة الكود هي نقله إلى الدلائل الصحيحة.

1. ستتعلم



2. قصة حقيقية لمهندس برمجيات الخلفية

(1) المشكلة: توجد 100,000 سطر من التعليمات البرمجية كلها في ملف main.go

تولت أليس إدارة مشروع للتجارة الإلكترونية:

«قام سلفُّي بوضع كل الكود في main.go: تسجيل المسارات، وعمليات قاعدة البيانات، وقوالب HTML، والمنطق التشغيلي — كلها مختلطة معًا في 30,000 سطر. وعندما أردت إضافة حقل لحالة المستخدم، اضطررت إلى إجراء تغييرات في خمسة أماكن مختلفة حتى أتمكن من تنفيذ ذلك بشكل صحيح. استغرق الأمر مني أسبوعين لإضافة ميزة واحدة، وثلاثة أيام فقط للعثور على الكود.»

GO
// Bad code: everything piled into main.go
package main

var db *sql.DB

func main() {
    // Database connection
    // Route registration
    // HTML templates
    // User handler
    // Order handler
    // Product handler
    // All mixed together!
}

// To find "user" related code: Ctrl+F search for "user", scattered across 50 places

(2) حل Go: بنية من 4 طبقات

TEXT 📖 للعرض فقط
myapp/
├── cmd/
│   └── server/
│       └── main.go         # Entry → dependency injection + start service
├── internal/
│   ├── handler/            # Layer 1: HTTP handlers (parse request / return response)
│   ├── service/            # Layer 2: Business logic (domain rules)
│   └── repository/         # Layer 3: Data access (database / external API)
├── pkg/
│   └── model/              # Layer 4: Domain models (data structures)
└── go.mod

(3) العوائد: الفوضى مقابل الطبقية

السيناريو البنية الفوضوية البنية ذات الأربع طبقات
حقول جديدة 5 تغييرات في مواقع غير معروفة تغييرات في النموذج والمعالج فقط
تبديل قواعد البيانات تحديث جميع استدعاءات قواعد البيانات تحديث طبقة المستودع فقط
اختبار الوحدة يتعذر إجراء اختبار الوحدة يمكن اختبار كل طبقة على حدة باستخدام نماذج محاكاة
دليل البدء للمبتدئين تصفح 30,000 سطر من ملف main.go فهم المسؤوليات بمجرد النظر إلى أسماء المجلدات


3. التصميم القياسي للمشروع

(1) شرح مفصل لهيكل الدلائل

TEXT 📖 للعرض فقط
myproject/
├── cmd/                    # Executable entry points
│   ├── server/             #   server binary
│   │   └── main.go
│   └── migrate/            #   database migration tool
│       └── main.go
├── internal/               # Private packages (cannot be imported externally)
│   ├── handler/            #   HTTP handlers
│   ├── service/            #   Business logic
│   └── repository/         #   Data access
├── pkg/                    # Exportable public packages
│   └── model/              #   Domain models
├── migrations/             # SQL migration files
├── config/                 # Configuration files
├── scripts/                # Helper scripts
├── go.mod
└── go.sum

(2) cmd/ / internal/ / pkg/ المسؤوليات

الدليل الاستخدامات يمكن استيرادها من مصدر خارجي
cmd/ نقطة الدخول القابلة للتنفيذ (الدالة الرئيسية) غير متوفر (إنها الحزمة الرئيسية)
internal/ تنفيذ خاص ❌ يحظر مُركِّب لغة Go الاستيرادات الخارجية
pkg/ رمز عام مكشوف للعالم الخارجي
💡 نصيحة: الدليل internal هو دليل خاص بمترجم لغة Go — ولا يمكن لأي حزمة خارج الدليل الأصلي لـ internal استيراده. وهذا يوفر تغليفًا حقيقيًّا، وهو أكثر أمانًا من «قواعد التسمية» (مثل _private).



4. نموذج «الهندسة النظيفة» المكون من 4 طبقات

TEXT 📖 للعرض فقط
handler (HTTP) → service (business) → repository (data)
     ↓                ↓                ↓
   Request parsing   Domain rules     Database/API
   Response return   Transaction mgmt CRUD operations
    Param validation  Multi-step orchestration  Cache access

▶ مثال: تنفيذ مكون من 4 طبقات

GO 📖 للعرض فقط
// ---------- Layer 4: Model (domain models) ----------
// pkg/model/user.go
package model

type User struct {
    ID        int
    Name      string
    Email     string
    CreatedAt time.Time
}

type CreateUserRequest struct {
    Name  string
    Email string
}

// ---------- Layer 3: Repository (data access) ----------
// internal/repository/user.go
package repository

import (
    "database/sql"
    "myapp/pkg/model"
)

type UserRepository interface {
    FindByID(id int) (*model.User, error)
    Create(req model.CreateUserRequest) (*model.User, error)
    List() ([]*model.User, error)
    Delete(id int) error
}

type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(id int) (*model.User, error) {
    row := r.db.QueryRow("SELECT id, name, email, created_at FROM users WHERE id = ?", id)
    user := &model.User{}
    err := row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return user, err
}

func (r *userRepository) Create(req model.CreateUserRequest) (*model.User, error) {
    result, err := r.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", req.Name, req.Email)
    if err != nil {
        return nil, err
    }
    id, _ := result.LastInsertId()
    return r.FindByID(int(id))
}

// ---------- Layer 2: Service (business logic) ----------
// internal/service/user.go
package service

import (
    "errors"
    "myapp/internal/repository"
    "myapp/pkg/model"
    "strings"
)

var (
    ErrUserNotFound    = errors.New("user not found")
    ErrInvalidName     = errors.New("name is required")
    ErrInvalidEmail    = errors.New("invalid email format")
)

type UserService struct {
    repo repository.UserRepository
}

func NewUserService(repo repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) Create(req model.CreateUserRequest) (*model.User, error) {
    if strings.TrimSpace(req.Name) == "" {
        return nil, ErrInvalidName
    }
    if !strings.Contains(req.Email, "@") {
        return nil, ErrInvalidEmail
    }
    return s.repo.Create(req)
}

func (s *UserService) Get(id int) (*model.User, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return nil, err
    }
    if user == nil {
        return nil, ErrUserNotFound
    }
    return user, nil
}

// ---------- Layer 1: Handler (HTTP handler) ----------
// internal/handler/user.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "strconv"

    "myapp/internal/service"
    "myapp/pkg/model"
)

type UserHandler struct {
    svc *service.UserService
}

func NewUserHandler(svc *service.UserService) *UserHandler {
    return &UserHandler{svc: svc}
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req model.CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    user, err := h.svc.Create(req)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, user)
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(r.PathValue("id"))

    user, err := h.svc.Get(id)
    if errors.Is(err, service.ErrUserNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "internal error")
        return
    }

    writeJSON(w, http.StatusOK, user)
}

// ---------- Utility functions ----------

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, msg string) {
    writeJSON(w, status, map[string]string{"error": msg})
}
131 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

(2) اتجاه التبعية

TEXT 📖 للعرض فقط
Handler → Service → Repository (→ DB)
   |          |           |
   ↓          ↓           ↓
  Model     Model       Model
الطبقة المسؤوليات التبعيات قابلية الاختبار
المُعالج تحليل HTTP/الاستجابة الخدمة الخدمة الوهمية
الخدمة القواعد التجارية/التنسيق المستودع مستودع النماذج الوهمية
مستودع عمليات CRUD للبيانات قاعدة البيانات/SQL قاعدة بيانات وهمية / اختبار التكامل
النموذج بنية البيانات لا شيء
🔥 خطأ شائع: يجب أن تتدفق التبعيات من الطبقة الخارجية إلى الطبقة الداخلية. يعرف «المعالج» (Handler) «الخدمة» (Service)، وتعرف «الخدمة» «المستودع» (Repository)، لكن «المستودع» يجب ألا يعرف «الخدمة». تعتمد كل طبقة فقط على الطبقة التي تليها، ويتم تحقيق الفصل بين الطبقات من خلال الواجهات (تجعل الواجهات الضمنية في لغة Go هذا الأمر يبدو طبيعيًا).



5. حقن التبعية

▶ مثال: حقن المنشئ

⚙️ المتطلبات الأساسية: قم بتشغيل go get github.com/mattn/go-sqlite3 (يتطلب CGO؛ أو استخدم modernc.org/sqlite كبديل للحصول على برنامج تشغيل مخصص للعبة «غو» فقط)

GO
// cmd/server/main.go
package main

import (
    "database/sql"
    "log"
    "net/http"

    "myapp/internal/handler"
    "myapp/internal/repository"
    "myapp/internal/service"
)

func main() {
    // ---- Dependency injection (assemble all layers) ----

    // 1. Database connection
    db, err := sql.Open("sqlite3", "./app.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 2. Repository layer
    userRepo := repository.NewUserRepository(db)
    orderRepo := repository.NewOrderRepository(db)

    // 3. Service layer (depends on Repository)
    userSvc := service.NewUserService(userRepo)
    orderSvc := service.NewOrderService(orderRepo, userRepo)

    // 4. Handler layer (depends on Service)
    userHandler := handler.NewUserHandler(userSvc)
    orderHandler := handler.NewOrderHandler(orderSvc)

    // 5. Route registration
    mux := http.NewServeMux()
    userHandler.Register(mux, "/api/v1/users")
    orderHandler.Register(mux, "/api/v1/orders")

    log.Println("Service listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
▶ جرّب الكود

(2) مقارنة بين مناهج حقن التبعية

الطريقة التنفيذ المزايا العيوب
الحقن في المنشئ NewXxx(dep) فحص صريح في وقت التحويل البرمجي كود طويل بسبب التبعيات
التنفيذ اليدوي تجميع الدالة الرئيسية لا حاجة إلى مكتبات خارجية صعوبة الصيانة في المشاريع الكبيرة
Google Wire توليد الكود الإدراج التلقائي منحنى التعلم
💡 نصيحة: يكفي استخدام الحقن اليدوي للتبعيات في المشاريع الصغيرة. عندما يحتوي مشروعك على 20 خدمة أو أكثر و50 تبعية أو أكثر، فكر في استخدام Google Wire (github.com/google/wire) لإنشاء كود حقن التبعيات تلقائيًا.



6. مثال كامل: إطار عمل مشروع التجارة الإلكترونية

▶ مثال: التنفيذ الكامل

GO 📖 للعرض فقط
// cmd/server/main.go
package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

// ---------- Model (pkg/model) ----------

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

type Order struct {
    ID        int       `json:"id"`
    UserID    int       `json:"user_id"`
    ProductID int       `json:"product_id"`
    Quantity  int       `json:"quantity"`
    Total     float64   `json:"total"`
    Status    string    `json:"status"`
    CreatedAt time.Time `json:"created_at"`
}

// ---------- Repository (internal/repository) ----------

type ProductRepository struct {
    db *sql.DB
}

func NewProductRepository(db *sql.DB) *ProductRepository {
    return &ProductRepository{db: db}
}

func (r *ProductRepository) FindByID(id int) (*Product, error) {
    p := &Product{}
    err := r.db.QueryRow("SELECT id, name, price FROM products WHERE id = ?", id).
        Scan(&p.ID, &p.Name, &p.Price)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return p, err
}

func (r *ProductRepository) List() ([]*Product, error) {
    rows, err := r.db.Query("SELECT id, name, price FROM products")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var products []*Product
    for rows.Next() {
        p := &Product{}
        if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
            return nil, err
        }
        products = append(products, p)
    }
    return products, rows.Err()
}

type OrderRepository struct {
    db *sql.DB
}

func NewOrderRepository(db *sql.DB) *OrderRepository {
    return &OrderRepository{db: db}
}

func (r *OrderRepository) Create(order *Order) error {
    result, err := r.db.Exec(
        "INSERT INTO orders (user_id, product_id, quantity, total, status) VALUES (?, ?, ?, ?, ?)",
        order.UserID, order.ProductID, order.Quantity, order.Total, order.Status,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    order.ID = int(id)
    return nil
}

func (r *OrderRepository) FindByID(id int) (*Order, error) {
    o := &Order{}
    err := r.db.QueryRow(
        "SELECT id, user_id, product_id, quantity, total, status, created_at FROM orders WHERE id = ?", id,
    ).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.Total, &o.Status, &o.CreatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return o, err
}

// ---------- Service (internal/service) ----------

type OrderService struct {
    productRepo *ProductRepository
    orderRepo   *OrderRepository
}

func NewOrderService(productRepo *ProductRepository, orderRepo *OrderRepository) *OrderService {
    return &OrderService{
        productRepo: productRepo,
        orderRepo:   orderRepo,
    }
}

type PlaceOrderInput struct {
    UserID    int
    ProductID int
    Quantity  int
}

var (
    ErrProductNotFound  = &AppError{Code: "PRODUCT_NOT_FOUND", Message: "product not found", HTTPStatus: 404}
    ErrInsufficientStock = &AppError{Code: "INSUFFICIENT_STOCK", Message: "insufficient stock", HTTPStatus: 409}
)

type AppError struct {
    Code       string `json:"code"`
    Message    string `json:"message"`
    HTTPStatus int    `json:"-"`
}

func (e *AppError) Error() string {
    return e.Message
}

func (s *OrderService) PlaceOrder(input PlaceOrderInput) (*Order, error) {
    product, err := s.productRepo.FindByID(input.ProductID)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }

    total := product.Price * float64(input.Quantity)

    order := &Order{
        UserID:    input.UserID,
        ProductID: input.ProductID,
        Quantity:  input.Quantity,
        Total:     total,
        Status:    "created",
    }

    if err := s.orderRepo.Create(order); err != nil {
        return nil, err
    }

    return order, nil
}

// ---------- Handler (internal/handler) ----------

type OrderHandler struct {
    svc *OrderService
}

func NewOrderHandler(svc *OrderService) *OrderHandler {
    return &OrderHandler{svc: svc}
}

func (h *OrderHandler) Register(mux *http.ServeMux, basePath string) {
    mux.HandleFunc("POST "+basePath, h.PlaceOrder)
    mux.HandleFunc("GET "+basePath+"/{id}", h.GetOrder)
}

type placeOrderRequest struct {
    ProductID int `json:"product_id"`
    Quantity  int `json:"quantity"`
}

func (h *OrderHandler) PlaceOrder(w http.ResponseWriter, r *http.Request) {
    // Get user ID from Context (injected by auth middleware)
    userID, ok := r.Context().Value("user_id").(int)
    if !ok {
        writeError(w, http.StatusUnauthorized, "not authenticated")
        return
    }

    var req placeOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    order, err := h.svc.PlaceOrder(PlaceOrderInput{
        UserID:    userID,
        ProductID: req.ProductID,
        Quantity:  req.Quantity,
    })

    if appErr, ok := err.(*AppError); ok {
        writeError(w, appErr.HTTPStatus, appErr.Message)
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "internal error")
        return
    }

    writeJSON(w, http.StatusCreated, order)
}

func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
    // ... business logic
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, msg string) {
    writeJSON(w, status, map[string]string{"error": msg})
}

// ---------- Main (dependency injection entry) ----------

func main() {
    db, err := sql.Open("sqlite3", "./shop.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Initialize tables
    db.Exec(`CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, price REAL
    )`)
    db.Exec(`CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER,
        product_id INTEGER, quantity INTEGER, total REAL,
        status TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )`)

    // Dependency injection
    productRepo := NewProductRepository(db)
    orderRepo := NewOrderRepository(db)
    orderSvc := NewOrderService(productRepo, orderRepo)
    orderHandler := NewOrderHandler(orderSvc)

    mux := http.NewServeMux()
    orderHandler.Register(mux, "/api/v1/orders")

    // Health check
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // Graceful shutdown
    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
        log.Println("Shutting down...")
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        server.Shutdown(ctx)
    }()

    log.Println("E-commerce service listening on :8080")
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
}
225 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
100%
graph TB
    subgraph Handler
        H[HTTP Handler]
    end
    subgraph Service
        S[Business Logic]
    end
    subgraph Repository
        R[Data Access]
    end
    subgraph Model
        M[Domain Structs]
    end
    subgraph External ["External Dependencies"]
        DB[(Database)]
    end

    H -->|calls| S
    S -->|calls| R
    R -->|queries| DB
    H --- M
    S --- M
    R --- M

    style Handler fill:#e1f5fe
    style Service fill:#fff3e0
    style Repository fill:#e8f5e9
    style Model fill:#f3e5f5
🔥 خطأ شائع: لا تتخطَّ أي طبقة. حتى لو كانت الخدمة تستدعي طريقة واحدة فقط في المستودع، يجب عليك المرور عبر طبقة الخدمة — فقواعد العمل ستتوسَّع في المستقبل. سيؤدي تخطي الطبقات إلى تشتت منطق العمل في جميع أنحاء المُعالج، مما يجعل إجراء الاختبارات الوحدوية أمراً مستحيلاً.



❓ أسئلة شائعة

س ما هي «الهندسة النظيفة»؟
ج نمط هندسي متعدد الطبقات — من الخارج إلى الداخل: المعالج (طبقة الواجهة) → الخدمة (طبقة حالات الاستخدام) → المستودع (طبقة البيانات) → النموذج (طبقة المجال). المبدأ الأساسي: تتدفق التبعيات من الطبقات الخارجية إلى الطبقات الداخلية؛ ولا تدرك الطبقات الداخلية وجود الطبقات الخارجية. وتقوم الطبقات الخارجية بتنفيذ الواجهات، بينما تقوم الطبقات الداخلية بتعريف الواجهات.
س كيف يتم تنظيم الطبقات في النموذج المكون من 4 طبقات؟
ج (1) المعالج (Handler): تحليل طلبات HTTP والاستجابة لها، والتحقق من صحة المعلمات؛ (2) الخدمة (Service): قواعد العمل، والتنسيق متعدد الخطوات، والمعاملات؛ (3) المستودع (Repository): عمليات CRUD في قاعدة البيانات، والوصول إلى ذاكرة التخزين المؤقت، وواجهات برمجة التطبيقات الخارجية؛ (4) النموذج (Model): تعريفات هياكل البيانات. تعتمد كل طبقة فقط على الطبقة التي تليها، وهي منفصلة عن الطبقات الأخرى من خلال الواجهات.
س كيف يتم تنفيذ حقن التبعيات؟
ج يُعد حقن المنشئ النهج الأكثر شيوعًا: NewXxx(dep1, dep2) *Xxx. يتم إنشاء جميع التبعيات وتجميعها في الدالة الرئيسية (أو في الكود الذي يولده Wire). المزايا: (1) علاقات تبعيات واضحة؛ (2) فحوصات في وقت التحويل البرمجي؛ (3) القدرة على تمرير نماذج محاكاة أثناء اختبار الوحدة.
س لماذا نستخدم الدليل internal؟
ج يفرض مُترجم لغة Go أن الحزم الموجودة في الدليل internal لا يمكن استيرادها إلا من خلال الكود الموجود في الدليل الأصلي الخاص بها. وهذا يوفر تغليفًا حقيقيًّا — حيث لا يمكن للمستخدمين الخارجيين استيراد حزم internal. وفي المشاريع الكبيرة، يمنع هذا حدوث انتهاكات في بنية النظام (مثل استيراد handlers مباشرةً إلى repository).
س كيف ينبغي تصميم أنواع الأخطاء؟
ج قم بتعريف أنواع أخطاء مخصصة تتضمن حقول «Code» و«Message» و«HTTPStatus». تقوم طبقة الخدمة بإرجاع أخطاء الأعمال (مثل ErrProductNotFound)، بينما تقوم طبقة المعالج بتعيين هذه الأخطاء إلى رموز حالة HTTP واستجابات JSON. المزايا: يتيح تنسيق الأخطاء الموحد فهم أنواع الأخطاء على الفور.
س هل من الضروري استخدام الواجهات بين الطبقات؟
ج يُنصح بذلك. تجعل الواجهات الضمنية في لغة Go عملية عكس التبعية تبدو طبيعية — حيث يُعرّف المستودع (Repository) واجهةً ما، ويعتمد المعالج (Handler) على تلك الواجهة، ويتم حقن التنفيذ الملموس عبر المنشئ. الفوائد: (1) يمكن لاختبارات الوحدة استخدام نماذج محاكاة؛ (2) لا يتطلب تبديل التنفيذات (على سبيل المثال، SQLite → MySQL) إجراء أي تغييرات على المُستدعي.
س هل تحتاج المشاريع الصغيرة أيضًا إلى بنية من 4 طبقات؟
ج لا. حجم المشروع هو الذي يحدد عمق البنية — استخدم بنية مسطحة للمشاريع التي يقل عدد أسطر الكود فيها عن 500 سطر، وبنية من طبقتين (معالج + مستودع) للمشاريع التي يتراوح عدد أسطر الكود فيها بين 500 و5,000 سطر، وبنية من 4 طبقات للمشاريع التي يزيد عدد أسطر الكود فيها عن 5,000 سطر. يُنصح بالبدء بهندسة ثنائية الطبقات وإضافة الطبقات تدريجيًا مع نمو المشروع. تجنب الإفراط في الهندسة.

📖 ملخص


📝 تمارين

  1. أساسي (مستوى الصعوبة ⭐): أنشئ هيكلاً أساسياً للمشروع يحتوي على المجلدات التالية: cmd/server/main.go، و internal/handler/، و internal/service/، و internal/repository/، و pkg/model/. قم بتنفيذ HealthHandler بسيط يعيد {"status": "ok"}.

  2. متقدم (صعوبة ⭐⭐): تنفيذ إدارة فئات المنتجات باستخدام بنية من 4 مستويات. المتطلبات: (1) النموذج: Category(id, name, parent_id)؛ (2) المعالج: نقاط نهاية CRUD؛ (3) الخدمة: التحقق من تفرد أسماء الفئات ومنع المراجع الدائرية؛ (4) المستودع: يتم تنفيذه باستخدام SQLite؛ (5) تقوم الدالة main بتجميع المكونات عبر حقن المنشئ.

  3. التحدي (الصعوبة ⭐⭐⭐): أعد هيكلة نظام إدارة المكتبة من الدرس 22 ليصبح بنية من أربعة مستويات. المتطلبات: (1) انقل الكود إلى الدليل cmd/internal/pkg؛ (2) يجب أن تتعامل طبقة المعالج (Handler) مع بروتوكول HTTP فقط؛ (3) يجب أن تتضمن طبقة الخدمة (Service) عملية التحقق الكاملة من صحة الأعمال؛ (4) يجب أن تستخدم طبقة المستودع (Repository) SQLite؛ (5) اجعل المستودع (Repository) متاحًا كواجهة لتمكين الاختبار الوهمي؛ (6) اكتب اختبارات الوحدة (باستخدام مستودع وهمي) لاختبار قواعد الأعمال في طبقة الخدمة.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%