الحزم والوحدات
الدرس 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:
package main // حزمة الدخول للبرامج التنفيذية
package mathutil // حزمة الأدوات
.go في نفس الدليل يجب أن تُعلن نفس اسم الحزمة (عادة يطابق اسم الدليل).
2. تهيئة وحدة
# نفّذ في جذر المشروع
go mod init my-module
# يُنشئ ملف go.mod:
# module my-module
#
# go 1.24
github.com/username/my-module، لتسهيل الإشارة إليها.
3. تنظيم التبعيات
# إضافة التبعيات المفقودة، إزالة غير المستخدمة
go mod tidy
go mod tidy في كل مرة تُقدّم فيها حزمة طرف ثالث جديدة.
4. عبارات import
import "fmt" // المكتبة القياسية
import "github.com/gin-gonic/gin" // حزمة طرف ثالث
// استيرادات مجمعة (موصى بها)
import (
"fmt"
"log"
"os"
)
5. قواعد التصدير
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. تثبيت حزم الطرف الثالث
# تحميل الحزمة وتحديث 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:
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:
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"))
}
التشغيل:
go run main.go
الناتج:
Hello, Alice! Welcome to Go programming.
Goodbye, Alice!
مثال: استخدام حزم الطرف الثالث (الصعوبة ⭐⭐)
تهيئة المشروع:
mkdir http-demo && cd http-demo
go mod init http-demo
go get github.com/gin-gonic/gin
main.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")
}
التشغيل:
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:
module bank-system
go 1.24
internal/validator/validator.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:
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:
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:
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)
}
}
التشغيل:
go run main.go
الناتج:
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:
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:
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")
}
الناتج:
[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:
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:
package model
// User نموذج
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
internal/repository/user_repo.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:
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)
}
}
الناتج:
Port: 8080
UserRepository{2 users total}
ID=1, Name=Alice, Email=alice@example.com
ID=2, Name=Bob, Email=bob@example.com
❓ أسئلة شائعة
س1: هل يجب أن تتطابق أسماء الحزم وأسماء الأدلة؟
ليس مُطبَّقًا إلزاميًا، لكن يُوصى بشدة. عادات Go تُحدد أن أسماء الحزم يجب أن تتطابق مع أسماء الأدلة. يمكن أن تختلف، لكنها تسبب ارتباكًا:
// myutil/calc.go
package calculator // اسم الحزمة لا يطابق اسم الدليل
// الاستيراد يستخدم المسار، لكن الاستخدام يستخدم اسم الحزمة
import "my-module/myutil" // المسار
calculator.Add(1, 2) // استخدام اسم الحزمة
س2: هل يمكنني استخدام أي شيء لمسار go mod init؟
نعم، لكن استخدام مسار مستودع يُوصى به:
# ✅ مُوصى به: سهل للآخرين للإشارة
go mod init github.com/yourname/yourproject
# ⚠️ مقبول أيضًا: مشاريع محلية
go mod init myproject
# ❌ تجنب: أحرف خاصة أو مسافات
go mod init "my project"
س3: لماذا ملف go.sum مطلوب؟
go.sum يُسجّل التجزئة التشفيرية لكل تبعية، مما يضمن أن الكود الذي تم تحميله لم يتعرض للتلاعب. يجب إضافته إلى التحكم في الإصدار مع go.mod:
# go.mod — يُعلن التبعيات والإصدارات
# go.sum — يتحقق من سلامة التبعيات
# كلاهما ضروري
git add go.mod go.sum
س4: كيف أُنظّم عدة ملفات في نفس الحزمة؟
جميع ملفات .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 بالدوال المُصدَّرة التالية:
Reverse(s string) string— عكس سلسلةToUpper(s string) string— تحويل إلى أحرف كبيرةWordCount(s string) int— عدد الكلمات
ثم استوردها واستخدمها في main.go.
تمرين 2: ممارسة الحزمة الداخلية
أنشئ مشروعًا يحتوي حزمة internal/config لقراءة الإعدادات (كـ struct)، وحزمة app تستخدم الإعدادات. تحقق: حزمة app يمكنها استيراد internal/config، لكن الحزم الخارجية لا تستطيع.
تمرين 3: آلة حاسبة متعددة الحزم
صمم مشروع آلة حاسبة بالحزم التالية:
calc/operation— العمليات الأساسية (جمع، طرح، ضرب، قسمة)calc/scientific— العمليات العلمية (أُسس، عوامل)internal/validator— التحقق من المدخلات (فحص القسمة على صفر، فحص الأرقام السالبة، إلخ)
المتطلبات: حزمة scientific يمكنها استدعاء دوال حزمة operation، وجميع منطق التحقق يوضع في internal/validator.



