الحزم والوحدات

الدرس 11: الحزم والوحدات

تشبيه من الواقع: تخيل مكتبة كبيرة. كل كتاب يُوضع على رفوف مختلفة حسب الفئة — الأدب، العلم، التاريخ... هذه "حزمة." نظام فهرسة المكتبة يخبرك أين كل رف — هذه "وحدة." لا تحتاج لتكديس كل الكتب في غرفة واحدة؛ تصنيفها هو كيف تجد ما تحتاجه بكفاءة. نظام حزم هو تصنيف المكتبة لعالم الكود.


المفاهيم الأساسية

المفهوم الوصف
الحزمة وحدة تنظيم كود Go؛ جميع ملفات .go في دليل تنتمي لنفس الحزمة
الوحدة مجموعة من الحزم المرتبطة، مُعرَّفة بملف go.mod؛ أصغر وحدة لإدارة التبعيات
الاستيراد استخدام مُعرِّفات مُصدَّرة من حزم أخرى
قواعد التصدير المُعرِّفات التي تبدأ بحرف كبير يمكن الوصول إليها من حزم خارجية؛ حرف صغير يعني خاص بالحزمة
الحزمة الداخلية اسم دليل خاص؛ الحزم الداخلية يمكن استيرادها فقط من الكود في شجرة الأدلة الأبوية
go get تحميل وتثبيت حزم الطرف الثالث من مستودعات بعيدة

العلاقة بين الحزم والوحدات

my-module/              ← جذر الوحدة، يحتوي go.mod
├── go.mod
├── main.go             ← package main
├── mathutil/           ← حزمة فرعية mathutil
│   └── calc.go         ← package mathutil
└── internal/           ← حزمة داخلية
    └── secret.go       ← package internal

الصيغة الأساسية والاستخدام

1. إعلان package

كل ملف كود Go يجب أن يبدأ بإعلان package:

GO
package main // حزمة الدخول للبرامج التنفيذية

package mathutil // حزمة الأدوات
💡 نصيحة: جميع ملفات .go في نفس الدليل يجب أن تُعلن نفس اسم الحزمة (عادة يطابق اسم الدليل).

2. تهيئة وحدة

BASH
# نفّذ في جذر المشروع
go mod init my-module

# يُنشئ ملف go.mod:
# module my-module
#
# go 1.24
💡 نصيحة: مسارات الوحدات عادة عناوين مستودعات، مثل github.com/username/my-module، لتسهيل الإشارة إليها.

3. تنظيم التبعيات

BASH
# إضافة التبعيات المفقودة، إزالة غير المستخدمة
go mod tidy
💡 نصيحة: نفّذ go mod tidy في كل مرة تُقدّم فيها حزمة طرف ثالث جديدة.

4. عبارات import

GO
import "fmt"                    // المكتبة القياسية
import "github.com/gin-gonic/gin" // حزمة طرف ثالث

// استيرادات مجمعة (موصى بها)
import (
    "fmt"
    "log"
    "os"
)

5. قواعد التصدير

GO
package mathutil

var Pi = 3.14159      // يبدأ بحرف كبير → مُصدَّر، مرئي خارجيًا
var version = "1.0"   // يبدأ بحرف صغير → غير مُصدَّر، خاص بالحزمة فقط

func Add(a, b int) int { // حرف كبير → مُصدَّر
    return a + b
}

func subtract(a, b int) int { // حرف صغير → غير مُصدَّر
    return a - b
}
💡 نصيحة: قواعد التصدير تنطبق على المتغيرات، الثوابت، الأنواع، الدوال، حقول التراكيب، الأساليب — جميع المُعرِّفات تتبع هذه القاعدة.

6. الحزمة الداخلية

myapp/
├── internal/
│   └── auth/        ← فقط myapp/ و أدلة الفرعية يمكنها الاستيراد
│       └── auth.go
└── cmd/
    └── server/
        └── main.go ← ✅ يمكنها استيراد myapp/internal/auth
💡 نصيحة: internal هو ضبط وصول مُنفَّذ من المترجم، وليست عادة — أي حزمة خارج شجرة الأدلة الأبوية لا يمكنها استيرادها.

7. تثبيت حزم الطرف الثالث

BASH
# تحميل الحزمة وتحديث go.mod / go.sum
go get github.com/gin-gonic/gin

# تحديد إصدار
go get github.com/gin-gonic/gin@v1.9.1

# إزالة التبعيات غير المستخدمة
go mod tidy

الأمثلة

مثال: إنشاء واستخدام حزم مخصصة (الصعوبة ⭐)

هيكل المشروع:

greet-app/
├── go.mod
├── main.go
└── greeting/
    └── greet.go
▶ جرّب الكود

greeting/greet.go:

GO
package greeting

import "fmt"

// Hello يُرجع تحية (حرف كبير، مُصدَّر)
func Hello(name string) string {
    return fmt.Sprintf("Hello, %s! Welcome to Go programming.", name)
}

// farewell يُرجع رسالة وداع (حرف صغير، غير مُصدَّر)
func farewell(name string) string {
    return fmt.Sprintf("Goodbye, %s!", name)
}

// Goodbye هو غلاف مُصدَّر لـ farewell (حرف كبير، مُصدَّر)
func Goodbye(name string) string {
    return farewell(name)
}

main.go:

GO
package main

import (
    "fmt"
    "greet-app/greeting" // استيراد حزمة فرعية محلية
)

func main() {
    // استخدام الدوال المُصدَّرة
    fmt.Println(greeting.Hello("Alice"))
    fmt.Println(greeting.Goodbye("Alice"))

    // ❌ الكود التالي سيُسبب خطأ: farewell غير مُصدَّر
    // fmt.Println(greeting.farewell("Alice"))
}

التشغيل:

BASH
go run main.go

الناتج:

TEXT
Hello, Alice! Welcome to Go programming.
Goodbye, Alice!

مثال: استخدام حزم الطرف الثالث (الصعوبة ⭐⭐)

تهيئة المشروع:

BASH
mkdir http-demo && cd http-demo
go mod init http-demo
go get github.com/gin-gonic/gin
▶ جرّب الكود

main.go:

GO
package main

import (
    "net/http"

    "github.com/gin-gonic/gin" // إطار ويب من طرف ثالث
)

// User يُعرّف تراكيب مستخدم
type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    r := gin.Default() // إنشاء محرك افتراضي

    // طلب GET: إرجاع رسالة ترحيب
    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "Welcome to the Go package management tutorial!",
        })
    })

    // طلب GET: إرجاع معلومات المستخدم
    r.GET("/user", func(c *gin.Context) {
        user := User{
            Name:  "Alice",
            Email: "alice@example.com",
        }
        c.JSON(http.StatusOK, user)
    })

    // بدء الخادم، الاستماع على المنفذ 8080
    r.Run(":8080")
}

التشغيل:

BASH
go run main.go
# بعد بدء الخادم، زر http://localhost:8080

مثال: الحزمة الداخلية وتعاون الحزم المتعددة (الصعوبة ⭐⭐⭐)

هيكل المشروع:

bank-system/
├── go.mod
├── main.go
├── account/
│   └── account.go       ← حزمة الحساب (عامة)
├── internal/
│   └── validator/
│       └── validator.go ← حزمة المدقق (داخلية فقط)
└── transaction/
    └── transaction.go   ← حزمة المعاملات (عامة)
▶ جرّب الكود

go.mod:

TEXT
module bank-system

go 1.24

internal/validator/validator.go:

GO
package validator

import "errors"

// ValidateAmount يُحقّق من مبلغ المعاملة (حزمة داخلية فقط)
func ValidateAmount(amount float64) error {
    if amount <= 0 {
        return errors.New("amount must be greater than zero")
    }
    if amount > 1000000 {
        return errors.New("single transaction cannot exceed 1 million")
    }
    return nil
}

// ValidateAccountID يُحقّق من معرّف الحساب
func ValidateAccountID(id string) error {
    if len(id) < 6 {
        return errors.New("account ID must be at least 6 characters")
    }
    return nil
}

account/account.go:

GO
package account

import (
    "fmt"
    "bank-system/internal/validator" // استيراد الحزمة الداخلية
)

// Account تراكيب
type Account struct {
    ID      string
    Name    string
    Balance float64
}

// NewAccount يُنشئ حسابًا جديدًا
func NewAccount(id, name string, balance float64) (*Account, error) {
    // استخدام دالة التحقق من الحزمة الداخلية
    if err := validator.ValidateAccountID(id); err != nil {
        return nil, fmt.Errorf("account creation failed: %w", err)
    }
    if balance < 0 {
        return nil, fmt.Errorf("initial balance cannot be negative")
    }
    return &Account{
        ID:      id,
        Name:    name,
        Balance: balance,
    }, nil
}

// Deposit يودع أموالًا
func (a *Account) Deposit(amount float64) error {
    // استخدام دالة التحقق من الحزمة الداخلية
    if err := validator.ValidateAmount(amount); err != nil {
        return fmt.Errorf("deposit failed: %w", err)
    }
    a.Balance += amount
    return nil
}

// GetBalance يحصل على الرصيد
func (a *Account) GetBalance() float64 {
    return a.Balance
}

transaction/transaction.go:

GO
package transaction

import (
    "fmt"
    "bank-system/account"
    "bank-system/internal/validator" // يمكنها أيضًا استيراد الحزمة الداخلية
)

// Transfer دالة التحويل
func Transfer(from, to *account.Account, amount float64) error {
    // استخدام الحزمة الداخلية للتحقق من المبلغ
    if err := validator.ValidateAmount(amount); err != nil {
        return fmt.Errorf("transfer failed: %w", err)
    }

    if from.GetBalance() < amount {
        return fmt.Errorf("transfer failed: insufficient balance (current: %.2f)", from.GetBalance())
    }

    // تنفيذ التحويل
    if err := from.Deposit(-amount); err != nil {
        return fmt.Errorf("deduction failed: %w", err)
    }
    if err := to.Deposit(amount); err != nil {
        // تراجع
        from.Deposit(amount)
        return fmt.Errorf("credit failed: %w", err)
    }

    return nil
}

main.go:

GO
package main

import (
    "fmt"
    "bank-system/account"
    "bank-system/transaction"
    // ❌ الاستيراد التالي سيفشل: الحزمة الداخلية لا يمكن استيرادها خارجيًا
    // "bank-system/internal/validator"
)

func main() {
    // إنشاء حسابين
    acc1, err := account.NewAccount("ACC001", "Alice", 10000)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    acc2, err := account.NewAccount("ACC002", "Bob", 5000)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Printf("Before transfer - Alice: %.2f, Bob: %.2f\n", acc1.GetBalance(), acc2.GetBalance())

    // تنفيذ التحويل
    err = transaction.Transfer(acc1, acc2, 3000)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Printf("After transfer - Alice: %.2f, Bob: %.2f\n", acc1.GetBalance(), acc2.GetBalance())

    // اختبار مبلغ غير صالح
    err = acc1.Deposit(-100)
    if err != nil {
        fmt.Println("Expected error:", err)
    }
}

التشغيل:

BASH
go run main.go

الناتج:

TEXT
Before transfer - Alice: 10000.00, Bob: 5000.00
After transfer - Alice: 7000.00, Bob: 8000.00
Expected error: deposit failed: amount must be greater than zero

سيناريوهات التطبيق الواقعية

السيناريو 1: بناء حزمة سجل قابلة لإعادة الاستخدام

my-app/
├── go.mod
├── main.go
└── logger/
    └── logger.go

logger/logger.go:

GO
package logger

import (
    "fmt"
    "os"
    "time"
)

// Level مستوى السجل
type Level int

const (
    LevelDebug Level = iota // 0
    LevelInfo               // 1
    LevelWarn               // 2
    LevelError              // 3
)

// Logger تراكيب
type Logger struct {
    prefix string
    level  Level
}

// New يُنشئ كائن مسجل
func New(prefix string, level Level) *Logger {
    return &Logger{
        prefix: prefix,
        level:  level,
    }
}

// log أسلوب داخلي لإخراج السجلات
func (l *Logger) log(level Level, tag, msg string) {
    if level < l.level {
        return
    }
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    line := fmt.Sprintf("[%s] [%s] [%s] %s\n", timestamp, tag, l.prefix, msg)
    os.Stdout.WriteString(line)
}

// Debug سجل تصحيح
func (l *Logger) Debug(msg string) {
    l.log(LevelDebug, "DEBUG", msg)
}

// Info سجل معلومات
func (l *Logger) Info(msg string) {
    l.log(LevelInfo, "INFO", msg)
}

// Warn سجل تحذير
func (l *Logger) Warn(msg string) {
    l.log(LevelWarn, "WARN", msg)
}

// Error سجل خطأ
func (l *Logger) Error(msg string) {
    l.log(LevelError, "ERROR", msg)
}

main.go:

GO
package main

import (
    "my-app/logger"
)

func main() {
    log := logger.New("APP", logger.LevelInfo)

    log.Debug("This won't show because level is below Info")
    log.Info("Application started")
    log.Warn("Disk space low")
    log.Error("Database connection failed")
}

الناتج:

TEXT
[2026-06-26 10:30:00] [INFO] [APP] Application started
[2026-06-26 10:30:00] [WARN] [APP] Disk space low
[2026-06-26 10:30:00] [ERROR] [APP] Database connection failed

السيناريو 2: تنظيم الحزم في بنية الطبقات

هيكل مشروع خدمة ويب نموذجي:

web-service/
├── go.mod
├── go.sum
├── main.go
├── config/
│   └── config.go        ← إدارة الإعدادات
├── internal/
│   ├── model/
│   │   └── user.go      ← نموذج البيانات
│   ├── repository/
│   │   └── user_repo.go ← طبقة الوصول للبيانات
│   └── service/
│       └── user_service.go ← طبقة منطق الأعمال
└── handler/
    └── user_handler.go  ← طبقة معالج HTTP

config/config.go:

GO
package config

// Config إعدادات التطبيق
type Config struct {
    Port     string
    DBHost   string
    DBPort   int
    LogLevel string
}

// Load يُحمّل الإعدادات (مثال مبسط)
func Load() *Config {
    return &Config{
        Port:     "8080",
        DBHost:   "localhost",
        DBPort:   5432,
        LogLevel: "info",
    }
}

internal/model/user.go:

GO
package model

// User نموذج
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

internal/repository/user_repo.go:

GO
package repository

import (
    "fmt"
    "web-service/internal/model"
)

// UserRepository مستودع بيانات المستخدمين
type UserRepository struct {
    users  map[int]*model.User
    nextID int
}

// NewUserRepository يُنشئ كائن مستودع
func NewUserRepository() *UserRepository {
    return &UserRepository{
        users:  make(map[int]*model.User),
        nextID: 1,
    }
}

// Create يُنشئ مستخدمًا
func (r *UserRepository) Create(name, email string) *model.User {
    user := &model.User{
        ID:    r.nextID,
        Name:  name,
        Email: email,
    }
    r.users[r.nextID] = user
    r.nextID++
    return user
}

// FindByID يبحث عن مستخدم بالمعرّف
func (r *UserRepository) FindByID(id int) (*model.User, bool) {
    user, ok := r.users[id]
    return user, ok
}

// FindAll يجد جميع المستخدمين
func (r *UserRepository) FindAll() []*model.User {
    result := make([]*model.User, 0, len(r.users))
    for _, u := range r.users {
        result = append(result, u)
    }
    return result
}

// String يُرجع ملخص المستودع
func (r *UserRepository) String() string {
    return fmt.Sprintf("UserRepository{%d users total}", len(r.users))
}

main.go:

GO
package main

import (
    "fmt"
    "web-service/config"
    "web-service/internal/repository"
)

func main() {
    // تحميل الإعدادات
    cfg := config.Load()
    fmt.Println("Port:", cfg.Port)

    // استخدام المستودع
    repo := repository.NewUserRepository()
    repo.Create("Alice", "alice@example.com")
    repo.Create("Bob", "bob@example.com")

    fmt.Println(repo)

    // العثور على جميع المستخدمين
    for _, u := range repo.FindAll() {
        fmt.Printf("  ID=%d, Name=%s, Email=%s\n", u.ID, u.Name, u.Email)
    }
}

الناتج:

TEXT
Port: 8080
UserRepository{2 users total}
  ID=1, Name=Alice, Email=alice@example.com
  ID=2, Name=Bob, Email=bob@example.com

❓ أسئلة شائعة

س1: هل يجب أن تتطابق أسماء الحزم وأسماء الأدلة؟

ليس مُطبَّقًا إلزاميًا، لكن يُوصى بشدة. عادات Go تُحدد أن أسماء الحزم يجب أن تتطابق مع أسماء الأدلة. يمكن أن تختلف، لكنها تسبب ارتباكًا:

GO
// myutil/calc.go
package calculator // اسم الحزمة لا يطابق اسم الدليل

// الاستيراد يستخدم المسار، لكن الاستخدام يستخدم اسم الحزمة
import "my-module/myutil" // المسار
calculator.Add(1, 2)      // استخدام اسم الحزمة

س2: هل يمكنني استخدام أي شيء لمسار go mod init؟

نعم، لكن استخدام مسار مستودع يُوصى به:

BASH
# ✅ مُوصى به: سهل للآخرين للإشارة
go mod init github.com/yourname/yourproject

# ⚠️ مقبول أيضًا: مشاريع محلية
go mod init myproject

# ❌ تجنب: أحرف خاصة أو مسافات
go mod init "my project"

س3: لماذا ملف go.sum مطلوب؟

go.sum يُسجّل التجزئة التشفيرية لكل تبعية، مما يضمن أن الكود الذي تم تحميله لم يتعرض للتلاعب. يجب إضافته إلى التحكم في الإصدار مع go.mod:

BASH
# go.mod — يُعلن التبعيات والإصدارات
# go.sum — يتحقق من سلامة التبعيات
# كلاهما ضروري
git add go.mod go.sum

س4: كيف أُنظّم عدة ملفات في نفس الحزمة؟

جميع ملفات .go في نفس الدليل تتشارك نفس الحزمة ويمكنها الإشارة لبعضها مباشرة دون استيراد:

GO
// mathutil/add.go
package mathutil

func Add(a, b int) int { return a + b }

// mathutil/multiply.go — نفس الحزمة، يمكنها استخدام Add مباشرة
package mathutil

func Multiply(a, b int) int {
    result := 0
    for i := 0; i < b; i++ {
        result = Add(result, a) // استدعاء مباشر، لا حاجة لاستيراد
    }
    return result
}

📖 ملخص

النقطة الرئيسية المحتوى
إعلان package كل ملف .go يجب أن يُعلن اسم حزمته في السطر الأول
تهيئة الوحدة go mod init <module-path> يُنشئ go.mod
إدارة التبعيات go mod tidy يُنظف التبعيات، go get يُثبّت حزم الطرف الثالث
قواعد التصدير حرف كبير = مُصدَّر، حرف صغير = خاص بالحزمة فقط
الحزمة الداخلية ضبط وصول مُنفَّذ من المترجم، قابل للاستيراد فقط من شجرة الأدلة الأبوية
أفضل الممارسات اسم الحزمة يطابق اسم الدليل، تنظيم حسب المسؤولية، تجنب التبعيات الدائرية

📝 تمارين

تمرين 1: إنشاء حزمة أدوات نصية

أنشئ حزمة stringutil بالدوال المُصدَّرة التالية:

ثم استوردها واستخدمها في main.go.

تمرين 2: ممارسة الحزمة الداخلية

أنشئ مشروعًا يحتوي حزمة internal/config لقراءة الإعدادات (كـ struct)، وحزمة app تستخدم الإعدادات. تحقق: حزمة app يمكنها استيراد internal/config، لكن الحزم الخارجية لا تستطيع.

تمرين 3: آلة حاسبة متعددة الحزم

صمم مشروع آلة حاسبة بالحزم التالية:

المتطلبات: حزمة scientific يمكنها استدعاء دوال حزمة operation، وجميع منطق التحقق يوضع في internal/validator.


الدرس التالي: ممارسة التراكيب ←

Web-Tutorial.com

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

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

100%