Go: بنية مشروع Go
آخر تحديث: 2026-08-26
لا تفرض لغة «Go» هيكلًا محددًا للمشروع من خلال إطار عمل ما — لكن هذا يعني أنه يتعين عليك تصميمه بنفسك. فالبنية المدروسة جيدًا تحافظ على وضوح مشروعك عندما يبلغ طوله 1,000 سطر، وتجعله قابلاً للإدارة حتى عندما يصل إلى 100,000 سطر.
عندما تتولى مسؤولية مشروع يكون فيه كل الكود محشورًا في main.go، فإن الخطوة الأولى في إعادة هيكلة الكود هي نقله إلى الدلائل الصحيحة.
1. ستتعلم
- الهيكل القياسي لمجلدات مشروع Go:
cmd/،internal/،pkg/ - نموذج «الهندسة النظيفة» المكون من 4 طبقات
- حقن التبعية (حقن المنشئ)
- تعريف نوع غير صحيح
- إعداد إطار عمل المشروع
2. قصة حقيقية لمهندس برمجيات الخلفية
(1) المشكلة: توجد 100,000 سطر من التعليمات البرمجية كلها في ملف main.go
تولت أليس إدارة مشروع للتجارة الإلكترونية:
«قام سلفُّي بوضع كل الكود في
main.go: تسجيل المسارات، وعمليات قاعدة البيانات، وقوالب HTML، والمنطق التشغيلي — كلها مختلطة معًا في 30,000 سطر. وعندما أردت إضافة حقل لحالة المستخدم، اضطررت إلى إجراء تغييرات في خمسة أماكن مختلفة حتى أتمكن من تنفيذ ذلك بشكل صحيح. استغرق الأمر مني أسبوعين لإضافة ميزة واحدة، وثلاثة أيام فقط للعثور على الكود.»
// 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 طبقات
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) شرح مفصل لهيكل الدلائل
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 طبقات
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 طبقات
// ---------- 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})
}
(2) اتجاه التبعية
Handler → Service → Repository (→ DB)
| | |
↓ ↓ ↓
Model Model Model
| الطبقة | المسؤوليات | التبعيات | قابلية الاختبار |
|---|---|---|---|
| المُعالج | تحليل HTTP/الاستجابة | الخدمة | الخدمة الوهمية |
| الخدمة | القواعد التجارية/التنسيق | المستودع | مستودع النماذج الوهمية |
| مستودع | عمليات CRUD للبيانات | قاعدة البيانات/SQL | قاعدة بيانات وهمية / اختبار التكامل |
| النموذج | بنية البيانات | لا شيء | — |
5. حقن التبعية
▶ مثال: حقن المنشئ
⚙️ المتطلبات الأساسية: قم بتشغيل
go get github.com/mattn/go-sqlite3(يتطلب CGO؛ أو استخدمmodernc.org/sqliteكبديل للحصول على برنامج تشغيل مخصص للعبة «غو» فقط)
// 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 | توليد الكود | الإدراج التلقائي | منحنى التعلم |
github.com/google/wire) لإنشاء كود حقن التبعيات تلقائيًا.
6. مثال كامل: إطار عمل مشروع التجارة الإلكترونية
▶ مثال: التنفيذ الكامل
// 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)
}
}
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
❓ أسئلة شائعة
NewXxx(dep1, dep2) *Xxx. يتم إنشاء جميع التبعيات وتجميعها في الدالة الرئيسية (أو في الكود الذي يولده Wire). المزايا: (1) علاقات تبعيات واضحة؛ (2) فحوصات في وقت التحويل البرمجي؛ (3) القدرة على تمرير نماذج محاكاة أثناء اختبار الوحدة.internal؟internal لا يمكن استيرادها إلا من خلال الكود الموجود في الدليل الأصلي الخاص بها. وهذا يوفر تغليفًا حقيقيًّا — حيث لا يمكن للمستخدمين الخارجيين استيراد حزم internal. وفي المشاريع الكبيرة، يمنع هذا حدوث انتهاكات في بنية النظام (مثل استيراد handlers مباشرةً إلى repository).📖 ملخص
- التخطيط القياسي:
cmd/(نقطة الدخول)،internal/(خاص)،pkg/(عام) - نموذج من 4 طبقات: المُعالج → الخدمة → المستودع → النموذج
- اتجاه التبعية: من الطبقة الخارجية إلى الطبقة الداخلية؛ فالطبقة الداخلية لا تعلم بوجود الطبقة الخارجية
- حقن التبعيات: حقن المنشئ (الأكثر شيوعًا)
- فصل الواجهات: تجعل الواجهات الضمنية في لغة Go عملية انعكاس التبعية أمراً طبيعياً
- نوع الخطأ: خطأ مخصص في التطبيق (AppError) يحتوي على الرمز/الرسالة/حالة HTTP
- الدليل
internal: التغليف المفروض بواسطة مُترجم لغة Go
📝 تمارين
-
أساسي (مستوى الصعوبة ⭐): أنشئ هيكلاً أساسياً للمشروع يحتوي على المجلدات التالية: cmd/server/main.go، و internal/handler/، و internal/service/، و internal/repository/، و pkg/model/. قم بتنفيذ
HealthHandlerبسيط يعيد{"status": "ok"}. -
متقدم (صعوبة ⭐⭐): تنفيذ إدارة فئات المنتجات باستخدام بنية من 4 مستويات. المتطلبات: (1) النموذج: Category(id, name, parent_id)؛ (2) المعالج: نقاط نهاية CRUD؛ (3) الخدمة: التحقق من تفرد أسماء الفئات ومنع المراجع الدائرية؛ (4) المستودع: يتم تنفيذه باستخدام SQLite؛ (5) تقوم الدالة
mainبتجميع المكونات عبر حقن المنشئ. -
التحدي (الصعوبة ⭐⭐⭐): أعد هيكلة نظام إدارة المكتبة من الدرس 22 ليصبح بنية من أربعة مستويات. المتطلبات: (1) انقل الكود إلى الدليل
cmd/internal/pkg؛ (2) يجب أن تتعامل طبقة المعالج (Handler) مع بروتوكول HTTP فقط؛ (3) يجب أن تتضمن طبقة الخدمة (Service) عملية التحقق الكاملة من صحة الأعمال؛ (4) يجب أن تستخدم طبقة المستودع (Repository) SQLite؛ (5) اجعل المستودع (Repository) متاحًا كواجهة لتمكين الاختبار الوهمي؛ (6) اكتب اختبارات الوحدة (باستخدام مستودع وهمي) لاختبار قواعد الأعمال في طبقة الخدمة.