معالجة الأخطاء

الدرس 10: معالجة الأخطاء

تشبيه من الواقع

تخيل أنك تذهب إلى مطعم لطلب طعام. يخبرك النادل: "عذرًا، هذا الطبق نفد اليوم." — هذه متوقعة؛ يمكنك طلب شيء آخر، وتستمر الحياة. هذا هو error في Go.

لكن إذا اشتعلت المطبخ فجأة ويجب على الجميع الإخلاء فورًا — هذه كارثة غير متوقعة. هذا هو panic في Go.

فلسفة معالجة الأخطاء في Go مباشرة: تعامل مع المشاكل المتوقعة بأناقة، ولا تنهار إلا لغير المتوقعة. على عكس اللغات الأخرى التي تمزج جميع الاستثناءات بـ try/catch، تتطلب Go منك معالجة كل خطأ بشكل صريح وفردي.


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

المفهوم الوصف
واجهة error نوع الخطأ المدمج في Go بأسلوب واحد فقط: Error() string
قيم الإرجاع المتعددة دوال Go عادةً تُرجع الأخطاء كقيمة إرجاع أخيرة
errors.New يُنشئ خطأ نصي بسيط
fmt.Errorf يُنشئ خطأ منسقًا (يمكن تغليفه بـ %w)
errors.Is يتحقق مما إذا كانت سلسلة الأخطاء تحتوي على خطأ محدد
errors.As يستخرج نوع خطأ محدد من سلسلة الأخطاء
panic يُطلق panic وقت التشغيل، مُتسببًا في انهيار البرنامج
recover يلتقط panic في prevent لمنع انهيار البرنامج
تغليف الأخطاء تغليف الأخطاء الأساسية بـ %w أو أنواع مخصصة للحفاظ على السياق

مخطط تدفق معالجة الأخطاء

الدالة تُرجع خطأ
       │
       ▼
  err != nil ?
  ┌────┴────┐
  │ نعم     │ لا
  ▼         ▼
معالجة الخطأ  المتابعة
  │
  ├─ قابل للتعافي → سجل / إرجاع قيمة افتراضية / إعادة المحاولة
  ├─ يحتاج للإبلاغ → تغليف وتمرير للأعلى
  └─ غير قابل للتعافي → panic

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

1. واجهة error

error هو نوع الواجهة المدمج في Go، مُعرَّف بإيجاز شديد:

GO
// تعريف واجهة error (مدمج، لا حاجة لاستيراد)
type error interface {
    Error() string
}

أي نوع يُنفذ أسلوب Error() string هو error.

2. إنشاء الأخطاء

GO
package main

import (
    "errors"
    "fmt"
)

func main() {
    // الطريقة 1: errors.new — إنشاء خطأ نصي بسيط
    err1 := errors.New("file not found")

    // الطريقة 2: fmt.Errorf — إنشاء خطأ منسق
    filename := "config.yaml"
    err2 := fmt.Errorf("failed to read file %s", filename)

    // الطريقة 3: fmt.Errorf + %w — تغليف الخطأ الأساسي (موصى به)
    baseErr := errors.New("permission denied")
    err3 := fmt.Errorf("cannot write log: %w", baseErr)

    fmt.Println(err1) // file not found
    fmt.Println(err2) // failed to read file config.yaml
    fmt.Println(err3) // cannot write log: permission denied
}

3. التحقق من الأخطاء

GO
package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("record not found")

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", ErrNotFound
    }
    return "Alice", nil
}

func main() {
    name, err := findUser(0)
    if err != nil {
        // errors.Is يتحقق مما إذا كانت سلسلة الأخطاء تحتوي على الخطأ المستهدف
        if errors.Is(err, ErrNotFound) {
            fmt.Println("User not found, please check the ID")
        } else {
            fmt.Println("Unknown error:", err)
        }
        return
    }
    fmt.Println("Found user:", name)
}

4. استخراج أنواع أخطاء محددة

GO
package main

import (
    "errors"
    "fmt"
)

// نوع خطأ مخصص
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed [%s]: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "age must be between 0 and 150",
        }
    }
    return nil
}

func main() {
    err := validateAge(200)
    if err != nil {
        // errors.As يستخرج نوعًا محددًا من سلسلة الأخطاء
        var ve *ValidationError
        if errors.As(err, &ve) {
            fmt.Printf("Field: %s, Reason: %s\n", ve.Field, ve.Message)
        } else {
            fmt.Println("Other error:", err)
        }
    }
}

5. panic و recover

GO
package main

import "fmt"

// safeDivide يستخدم recover لالتقاط panic
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("caught panic: %v", r)
        }
    }()

    // القسمة على صفر تُطلق panic
    return a / b, nil
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err) // Error: caught panic: runtime error: integer divide by zero
        return
    }
    fmt.Println("Result:", result)
}
💡 نصيحة: في Go، الغالبية العظمى من الأخطاء يجب معالجتها بإرجاع error، وليس panic. استخدم panic فقط عندما لا يستطيع البرنامج الاستمرار حقًا (مثل فشل التهيئة، أخطاء منطقية غير قابلة للتعافي).

💡 نصيحة: عند تغليف الأخطاء بـ fmt.Errorf، استخدم دائمًا %w بدل %v. %w يُبقي الخطأ الأصلي، مما يتيح لـ errors.Is و errors.As العمل بشكل صحيح.

💡 نصيحة: يجب أن تكون رسائل الأخطاء جمل صغيرة بدون علامات ترقيم، حيث غالبًا ما تُدمج في رسائل أكبر.


الأمثلة

مثال: معالجة أساسية للأخطاء (الصعوبة ⭐)

محاكاة قارئ ملف إعدادات بسيط لإظهار نمط إرجاع والتحقق من الخطأ الأكثر أساسية.

GO
package main

import (
    "errors"
    "fmt"
    "os"
)

// تعريف أخطاء رقيب على مستوى الحزمة
var (
    ErrFileNotFound  = errors.New("config file not found")
    ErrFileEmpty     = errors.New("config file is empty")
    ErrInvalidFormat = errors.New("invalid config file format")
)

// Config يمثل إعدادات التطبيق
type Config struct {
    Host string
    Port int
}

// LoadConfig يُحمّل الإعدادات من ملف (محاكاة)
func LoadConfig(path string) (*Config, error) {
    // محاكاة عدم العثور على الملف
    if path == "" {
        return nil, ErrFileNotFound
    }

    // محاكاة قراءة الملف
    data, err := os.ReadFile(path)
    if err != nil {
        // تغليف الخطأ الأساسي، الحفاظ على المعلومات الأصلية
        return nil, fmt.Errorf("reading config file %s: %w", path, err)
    }

    // التحقق مما إذا كان الملف فارغًا
    if len(data) == 0 {
        return nil, ErrFileEmpty
    }

    // محاكاة تحليل الإعدادات
    return &Config{
        Host: "localhost",
        Port: 8080,
    }, nil
}

func main() {
    // محاولة تحميل الإعدادات
    config, err := LoadConfig("app.conf")
    if err != nil {
        // استخدام errors.Is لتحديد نوع الخطأ
        switch {
        case errors.Is(err, ErrFileNotFound):
            fmt.Println("Error: Please specify the config file path")
        case errors.Is(err, ErrFileEmpty):
            fmt.Println("Error: Config file is empty, please check contents")
        case errors.Is(err, ErrInvalidFormat):
            fmt.Println("Error: Config file format is invalid")
        default:
            fmt.Println("Error:", err)
        }
        return
    }

    fmt.Printf("Config loaded successfully: %s:%d\n", config.Host, config.Port)
}
▶ جرّب الكود

الناتج (عندما لا يوجد الملف):

Error: Please specify the config file path

مثال: تغليف الأخطاء وفحص السلسلة (الصعوبة ⭐⭐)

يُظهر تغليف الأخطاء وانتشارها وفحص السلسلة في استدعاءات متعددة الطبقات.

GO
package main

import (
    "errors"
    "fmt"
)

// تعريف أخطاء الأعمال
var (
    ErrUserNotFound = errors.New("user not found")
    ErrPermission   = errors.New("insufficient permissions")
)

// يمثل أخطاء طبقة قاعدة البيانات
type DatabaseError struct {
    Operation string
    Table     string
    Err       error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error [%s.%s]: %e", e.Table, e.Operation, e.Err)
}

func (e *DatabaseError) Unwrap() error {
    return e.Err
}

// UserRepository مستودع المستخدمين
type UserRepository struct {
    users map[int]string
}

// FindByID يبحث عن مستخدم بالمعرّف
func (r *UserRepository) FindByID(id int) (string, error) {
    name, exists := r.users[id]
    if !exists {
        // تغليف كخطأ طبقة قاعدة البيانات
        return "", &DatabaseError{
            Operation: "SELECT",
            Table:     "users",
            Err:       ErrUserNotFound,
        }
    }
    return name, nil
}

// Service طبقة خدمة الأعمال
type Service struct {
    repo *UserRepository
}

// GetUser يحصل على معلومات المستخدم
func (s *Service) GetUser(id int, requireAdmin bool) (string, error) {
    name, err := s.repo.FindByID(id)
    if err != nil {
        // التغليف للأعلى، إضافة سياق الأعمال
        return "", fmt.Errorf("getting user info (id=%d): %w", id, err)
    }

    if requireAdmin && name != "admin" {
        return "", fmt.Errorf("user %s: %w", name, ErrPermission)
    }

    return name, nil
}

func main() {
    repo := &UserRepository{
        users: map[int]string{
            1: "admin",
            2: "Alice",
        },
    }
    svc := &Service{repo: repo}

    // سيناريوهات الاختبار
    testCases := []struct {
        name         string
        id           int
        requireAdmin bool
    }{
        {"Find admin", 1, false},
        {"Find regular user", 2, false},
        {"Find non-existent user", 99, false},
        {"Regular user accessing admin function", 2, true},
    }

    for _, tc := range testCases {
        fmt.Printf("--- %s ---\n", tc.name)
        user, err := svc.GetUser(tc.id, tc.requireAdmin)
        if err != nil {
            fmt.Printf("  Failed: %v\n", err)

            // errors.Is يمكنها عبر سلسلة الأخطاء للعثور على السبب الجذري
            if errors.Is(err, ErrUserNotFound) {
                fmt.Println("  Action: Prompt user to check ID")
            } else if errors.Is(err, ErrPermission) {
                fmt.Println("  Action: Prompt user to contact admin")
            }

            // errors.As يمكنها استخراج أنواع أخطاء محددة من الطبقات الوسطى
            var dbErr *DatabaseError
            if errors.As(err, &dbErr) {
                fmt.Printf("  DB details: table=%s, operation=%s\n", dbErr.Table, dbErr.Operation)
            }
        } else {
            fmt.Printf("  Success: User %s\n", user)
        }
        fmt.Println()
    }
}
▶ جرّب الكود

الناتج:

--- Find admin ---
  Success: User admin

--- Find regular user ---
  Success: User Alice

--- Find non-existent user ---
  Failed: getting user info (id=99): database error [users.SELECT]: user not found
  Action: Prompt user to check ID
  DB details: table=users, operation=SELECT

--- Regular user accessing admin function ---
  Failed: User Alice: insufficient permissions
  Action: Prompt user to contact admin

مثال: أنواع الأخطاء المخصصة و recover في الممارسة (الصعوبة ⭐⭐⭐)

تنفيذ معالج طلبات HTTP كامل مع أنواع أخطاء مخصصة وأكواد أخطاء وware وسطية استعادة panic.

GO
package main

import (
    "errors"
    "fmt"
    "runtime/debug"
)

// ==================== نظام الأخطاء المخصص ====================

// AppError خطأ على مستوى التطبيق مع كود خطأ وسياق
type AppError struct {
    Code    int               // كود خطأ الأعمال
    Message string            // رسالة ودية للمستخدم
    Detail  string            // معلومات تصحيح للمطور
    Cause   error             // السبب الأساسي
    Context map[string]string // سياق إضافي
}

func (e *AppError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Cause
}

// Is يُنفذ منطق مقارنة الأخطاء المخصص
func (e *AppError) Is(target error) bool {
    t, ok := target.(*AppError)
    if !ok {
        return false
    }
    return e.Code == t.Code
}

// NewAppError يُنشئ خطأ تطبيق جديد
func NewAppError(code int, message string) *AppError {
    return &AppError{
        Code:    code,
        Message: message,
        Context: make(map[string]string),
    }
}

// WithCause يُعيّن السبب الأساسي
func (e *AppError) WithCause(err error) *AppError {
    e.Cause = err
    return e
}

// WithDetail يُعيّن تفاصيل التصحيح
func (e *AppError) WithDetail(detail string) *AppError {
    e.Detail = detail
    return e
}

// WithContext يُضيف معلومات السياق
func (e *AppError) WithContext(key, value string) *AppError {
    e.Context[key] = value
    return e
}

// أكواد أخطاء مُعرَّفة مسبقًا
var (
    ErrBadRequest   = NewAppError(400, "bad request")
    ErrUnauthorized = NewAppError(401, "unauthorized access")
    ErrForbidden    = NewAppError(403, "forbidden")
    ErrNotFound     = NewAppError(404, "resource not found")
    ErrInternal     = NewAppError(500, "internal server error")
)

// ==================== طبقة الأعمال المحاكية ====================

// Request يُحاكي طلب HTTP
type Request struct {
    UserID  int
    Path    string
    IsAdmin bool
}

// Response يُحاكي استجابة HTTP
type Response struct {
    StatusCode int
    Body       string
}

// validateRequest يُحقّق من معلمات الطلب
func validateRequest(req *Request) error {
    if req.UserID <= 0 {
        return ErrBadRequest.
            WithDetail("user_id must be a positive integer").
            WithContext("user_id", fmt.Sprintf("%d", req.UserID))
    }
    if req.Path == "" {
        return ErrBadRequest.
            WithDetail("path cannot be empty")
    }
    return nil
}

// authenticate مصادقة
func authenticate(req *Request) error {
    if req.UserID == 0 {
        return ErrUnauthorized.WithDetail("missing authentication credentials")
    }
    return nil
}

// authorize تفويض
func authorize(req *Request) error {
    if !req.IsAdmin {
        return ErrForbidden.
            WithDetail("admin privileges required").
            WithContext("user_id", fmt.Sprintf("%d", req.UserID))
    }
    return nil
}

// findResource يبحث عن مورد (قد يُطلق panic)
func findResource(path string) (string, error) {
    // محاكاة خلل يُسبب panic
    if path == "/crash" {
        var p *int
        *p = 1 // إشارة nil، يُطلق panic
    }

    resources := map[string]string{
        "/users":   "User list",
        "/profile": "User profile",
    }

    data, exists := resources[path]
    if !exists {
        return "", ErrNotFound.
            WithDetail(fmt.Sprintf("resource for path %s not found", path))
    }
    return data, nil
}

// handleRequest يعالج الطلب (منطق الأعمال)
func handleRequest(req *Request) (*Response, error) {
    // 1. التحقق من المعلمات
    if err := validateRequest(req); err != nil {
        return nil, err
    }

    // 2. المصادقة
    if err := authenticate(req); err != nil {
        return nil, err
    }

    // 3. التفويض (فقط لمسارات الإدارة)
    if req.Path == "/admin" {
        if err := authorize(req); err != nil {
            return nil, err
        }
    }

    // 4. البحث عن المورد
    data, err := findResource(req.Path)
    if err != nil {
        return nil, err
    }

    return &Response{
        StatusCode: 200,
        Body:       data,
    }, nil
}

// ==================== وسطية الاستعادة ====================

// RecoveryMiddleware وسطية استعادة panic
func RecoveryMiddleware(handler func(*Request) (*Response, error)) func(*Request) (*Response, error) {
    return func(req *Request) (resp *Response, err error) {
        defer func() {
            if r := recover(); r != nil {
                // تسجيل تتبع panic
                stack := string(debug.Stack())
                fmt.Printf("[PANIC] %v\nStack:\n%s\n", r, stack)

                // إرجاع خطأ داخلي بدل الانهيار
                err = ErrInternal.
                    WithDetail(fmt.Sprintf("panic: %v", r)).
                    WithCause(fmt.Errorf("panic: %v", r))
            }
        }()

        return handler(req)
    }
}

// ==================== تنسيق الأخطاء ====================

// formatError يُنسّق الخطأ إلى استجابة ودية
func formatError(err error) *Response {
    var appErr *AppError
    if errors.As(err, &appErr) {
        msg := fmt.Sprintf("Error [%d]: %s", appErr.Code, appErr.Message)
        if appErr.Detail != "" {
            msg += fmt.Sprintf("\n  Detail: %s", appErr.Detail)
        }
        if len(appErr.Context) > 0 {
            msg += "\n  Context:"
            for k, v := range appErr.Context {
                msg += fmt.Sprintf("\n    %s: %s", k, v)
            }
        }
        return &Response{
            StatusCode: appErr.Code,
            Body:       msg,
        }
    }

    return &Response{
        StatusCode: 500,
        Body:       fmt.Sprintf("Unknown error: %v", err),
    }
}

// ==================== الدالة الرئيسية ====================

func main() {
    // تغليف المعالج بوسطية الاستعادة
    safeHandler := RecoveryMiddleware(handleRequest)

    // اختبار سيناريوهات متنوعة
    testCases := []struct {
        name string
        req  *Request
    }{
        {
            name: "Normal request",
            req:  &Request{UserID: 1, Path: "/users", IsAdmin: true},
        },
        {
            name: "Invalid parameters",
            req:  &Request{UserID: -1, Path: "/users"},
        },
        {
            name: "Unauthenticated",
            req:  &Request{UserID: 0, Path: "/users"},
        },
        {
            name: "Insufficient permissions",
            req:  &Request{UserID: 2, Path: "/admin", IsAdmin: false},
        },
        {
            name: "Resource not found",
            req:  &Request{UserID: 1, Path: "/unknown"},
        },
        {
            name: "Trigger panic (auto-recovery)",
            req:  &Request{UserID: 1, Path: "/crash"},
        },
    }

    for _, tc := range testCases {
        fmt.Printf("=== %s ===\n", tc.name)

        resp, err := safeHandler(tc.req)
        if err != nil {
            resp = formatError(err)
        }

        fmt.Printf("  Status: %d\n", resp.StatusCode)
        fmt.Printf("  Response: %s\n\n", resp.Body)
    }
}
▶ جرّب الكود

الناتج:

=== Normal request ===
  Status: 200
  Response: User list

=== Invalid parameters ===
  Status: 400
  Response: Error [400]: bad request
  Detail: user_id must be a positive integer
  Context:
    user_id: -1

=== Unauthenticated ===
  Status: 401
  Response: Error [401]: unauthorized access
  Detail: missing authentication credentials

=== Insufficient permissions ===
  Status: 403
  Response: Error [403]: forbidden
  Detail: admin privileges required
  Context:
    user_id: 2

=== Resource not found ===
  Status: 404
  Response: Error [404]: resource not found
  Detail: resource for path /unknown not found

=== Trigger panic (auto-recovery) ===
[PANIC] runtime error: invalid memory address or nil pointer dereference
Stack:
...
  Status: 500
  Response: Error [500]: internal server error
  Detail: panic: runtime error: invalid memory address or nil pointer dereference

سيناريوهات من الواقع

السيناريو 1: التراجع عن معاملة قاعدة البيانات

في عمليات قاعدة البيانات التي تتضمن عدة خطوات، أي فشل يتطلب التراجع عن العمليات المنفذة.

GO
package main

import (
    "errors"
    "fmt"
)

var (
    ErrBalanceInsufficient = errors.New("insufficient balance")
    ErrAccountFrozen       = errors.New("account is frozen")
)

// TxError خطأ المعاملة، يُسجّل الخطوة الفاشلة
type TxError struct {
    Step    string
    Cause   error
    Actions []string // الإجراءات المنفذة التي تحتاج تراجع
}

func (e *TxError) Error() string {
    return fmt.Sprintf("transaction failed [step: %s]: %v", e.Step, e.Cause)
}

func (e *TxError) Unwrap() error {
    return e.Cause
}

// TransferService خدمة التحويل
type TransferService struct {
    balances map[string]float64
    frozen   map[string]bool
}

// Transfer يُنفّذ معاملة تحويل
func (s *TransferService) Transfer(from, to string, amount float64) error {
    var executedSteps []string

    // الخطوة 1: التحقق من الحساب المصدر
    if s.frozen[from] {
        return &TxError{
            Step:  "validate source account",
            Cause: ErrAccountFrozen,
        }
    }
    executedSteps = append(executedSteps, "lock source account")

    // الخطوة 2: التحقق من الرصيد
    if s.balances[from] < amount {
        return &TxError{
            Step:    "check balance",
            Cause:   ErrBalanceInsufficient,
            Actions: executedSteps,
        }
    }
    executedSteps = append(executedSteps, "balance check passed")

    // الخطوة 3: الخصم من الحساب المصدر
    s.balances[from] -= amount
    executedSteps = append(executedSteps, fmt.Sprintf("deducted %.2f", amount))

    // الخطوة 4: التحقق من الحساب الهدف
    if s.frozen[to] {
        return &TxError{
            Step:    "validate target account",
            Cause:   ErrAccountFrozen,
            Actions: executedSteps, // يحتاج تراجع المبلغ المخصوم
        }
    }

    // الخطوة 5: الإضافة إلى الحساب الهدف
    s.balances[to] += amount

    fmt.Printf("  Transfer successful: %s -> %s, Amount: %.2f\n", from, to, amount)
    return nil
}

func main() {
    svc := &TransferService{
        balances: map[string]float64{
            "Alice": 1000,
            "Bob":   500,
            "Carol": 0,
        },
        frozen: map[string]bool{
            "Carol": true,
        },
    }

    tests := []struct {
        name string
        from string
        to   string
        amt  float64
    }{
        {"Normal transfer", "Alice", "Bob", 200},
        {"Insufficient balance", "Alice", "Bob", 9999},
        {"Target account frozen", "Alice", "Carol", 100},
    }

    for _, t := range tests {
        fmt.Printf("--- %s ---\n", t.name)
        fmt.Printf("  Before: %s=%.2f, %s=%.2f\n",
            t.from, svc.balances[t.from], t.to, svc.balances[t.to])

        err := svc.Transfer(t.from, t.to, t.amt)
        if err != nil {
            fmt.Printf("  Failed: %v\n", err)

            // التحقق مما إذا كان تراجع مطلوبًا
            var txErr *TxError
            if errors.As(err, &txErr) && len(txErr.Actions) > 0 {
                fmt.Printf("  Rollback %d executed actions:\n", len(txErr.Actions))
                for _, action := range txErr.Actions {
                    fmt.Printf("    - Undo: %s\n", action)
                }
                // في المشاريع الحقيقية، نفّذ منطق التراجع هنا
                // مثل: svc.balances[from] += amount
            }
        }

        fmt.Printf("  After: %s=%.2f, %s=%.2f\n\n",
            t.from, svc.balances[t.from], t.to, svc.balances[t.to])
    }
}

السيناريو 2: معالجة ملفات مجمعة مع جمع الأخطاء

عند معالجة عدة ملفات، لا تتوقف عند أول فشل. بدل ذلك، اجمع جميع الأخطاء وأبلغ عنها معًا.

GO
package main

import (
    "errors"
    "fmt"
    "strings"
)

var (
    ErrFileCorrupted = errors.New("file is corrupted")
    ErrPermission    = errors.New("insufficient permissions")
    ErrDiskFull      = errors.New("disk space insufficient")
)

// FileError خطأ ملف مفرد
type FileError struct {
    Path  string
    Op    string
    Cause error
}

func (e *FileError) Error() string {
    return fmt.Sprintf("[%s] %s: %v", e.Op, e.Path, e.Cause)
}

func (e *FileError) Unwrap() error {
    return e.Cause
}

// BatchResult نتيجة المعالجة المجمعة
type BatchResult struct {
    Success []string
    Failed  []FileError
}

func (r *BatchResult) HasErrors() bool {
    return len(r.Failed) > 0
}

func (r *BatchResult) Summary() string {
    var sb strings.Builder
    sb.WriteString(fmt.Sprintf("Processing complete: %d succeeded, %d failed\n",
        len(r.Success), len(r.Failed)))

    if len(r.Success) > 0 {
        sb.WriteString("Successful files:\n")
        for _, f := range r.Success {
            sb.WriteString(fmt.Sprintf("  ✓ %s\n", f))
        }
    }

    if len(r.Failed) > 0 {
        sb.WriteString("Failed files:\n")
        for _, e := range r.Failed {
            sb.WriteString(fmt.Sprintf("  ✗ %s (%s: %v)\n", e.Path, e.Op, e.Cause))
        }
    }

    return sb.String()
}

// processFile يُحاكي معالجة ملف مفرد
func processFile(path string) error {
    // محاكاة أخطاء مختلفة محتملة
    fileErrors := map[string]error{
        "corrupted.txt": ErrFileCorrupted,
        "secret.txt":    ErrPermission,
        "huge.txt":      ErrDiskFull,
    }

    if err, exists := fileErrors[path]; exists {
        return &FileError{
            Path:  path,
            Op:    "process",
            Cause: err,
        }
    }
    return nil
}

// processFiles يُعالج الملفات بشكل مجمع
func processFiles(paths []string) *BatchResult {
    result := &BatchResult{}

    for _, path := range paths {
        err := processFile(path)
        if err != nil {
            result.Failed = append(result.Failed, FileError{
                Path:  path,
                Op:    "process",
                Cause: errors.Unwrap(err),
            })
        } else {
            result.Success = append(result.Success, path)
        }
    }

    return result
}

func main() {
    files := []string{
        "report.pdf",
        "data.csv",
        "corrupted.txt",
        "image.png",
        "secret.txt",
        "huge.txt",
        "readme.md",
    }

    fmt.Printf("Starting to process %d files...\n\n", len(files))

    result := processFiles(files)
    fmt.Println(result.Summary())

    // تقديم اقتراحات مختلفة بناءً على أنواع الأخطاء
    for _, fe := range result.Failed {
        switch {
        case errors.Is(fe.Cause, ErrFileCorrupted):
            fmt.Printf("Suggestion: %s needs to be restored from backup\n", fe.Path)
        case errors.Is(fe.Cause, ErrPermission):
            fmt.Printf("Suggestion: Please check file permissions for %s\n", fe.Path)
        case errors.Is(fe.Cause, ErrDiskFull):
            fmt.Printf("Suggestion: Free up disk space and retry %s\n", fe.Path)
        }
    }
}

❓ أسئلة شائعة

س1: متى أستخدم panic مقابل إرجاع error؟

المبدأ: استخدم error للفشل المتوقع؛ استخدم panic لأخطاء البرمجة "التي يجب ألا تحدث أبدًا."

GO
// ✓ صحيح: إرجاع error — مدخلات المستخدم غير قابلة للتنبؤ
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divisor cannot be zero")
    }
    return a / b, nil
}

// ✓ صحيح: استخدام panic — هذا خلل في البرمجة، يجب ألا يُمرر nil أبدًا
func MustNotNil(v interface{}) {
    if v == nil {
        panic("value must not be nil, this is a caller's programming error")
    }
}

القواعد العملية:

س2: ما الفرق بين errors.Is و errors.As؟

GO
// errors.Is: تتحقق مما إذا كانت سلسلة الأخطاء تحتوي على خطأ رقيب محدد
// تعادل: err == target || err.Unwrap() == target || ...
errors.Is(err, ErrNotFound)  // true/false

// errors.As: تستخرج نوع خطأ محدد من سلسلة الأخطاء
// كتأكيد النوع، لكنها تجتاز سلسلة الأخطاء بالكامل
var appErr *AppError
errors.As(err, &appErr)  // true + تعيين، أو false

// تشبيه:
// errors.Is → "هل أنت أحمد؟" (فحص الهوية)
// errors.As → "هل أنت مبرمج؟ إذا نعم، أخبرني بمهاراتك" (استخراج معلومات)

س3: لماذا يُنصح ببدء رسائل الأخطاء بأحرف صغيرة بدون علامات ترقيم؟

عادة Go الرسمية ومجتمع المطورين:

GO
// ✓ مُوصى به
fmt.Errorf("reading file %s failed: %w", name, err)
// الناتج: reading file config.yaml failed: permission denied

// ✗ غير مُوصى به
fmt.Errorf("reading file %s failed.: %w", name, err)  // علامات ترقيم زائدة
fmt.Errorf("reading file %s failed!: %w", name, err)  // تعجب زائد
fmt.Errorf("Read file %s failed: %w", name, err)      // لغة غير متسقة

السبب هو أن الأخطاء غالبًا ما تُدمج في سلاسل أخطاء، والأحرف الصغيرة بدون علامات ترقيم أسهل للقراءة:

initializing database: connecting to postgres: dial tcp 127.0.0.1:5432: connection refused

س4: كيف أقارن أخطاء مغلفة؟

المقارنة المباشرة بـ == لن تعمل للأخطاء المغلفة؛ يجب استخدام errors.Is:

GO
var ErrSentinel = errors.New("sentinel")

wrapped := fmt.Errorf("context: %w", ErrSentinel)

// ✗ خاطئ: المقارنة المباشرة تفشل
fmt.Println(wrapped == ErrSentinel)  // false

// ✓ صحيح: استخدام errors.Is
fmt.Println(errors.Is(wrapped, ErrSentinel))  // true

// errors.Is تجتاز سلسلة الأخطاء بالكامل:
// wrapped → Unwrap() → ErrSentinel → تطابق!

📖 ملخص

الموضوع النقاط الرئيسية
واجهة error تحتاج فقط تنفيذ أسلوب Error() string
إنشاء الأخطاء errors.New للأخطاء البسيطة، fmt.Errorf + %w للأخطاء المغلفة
التحقق من الأخطاء errors.Is لفحص النوع، errors.As لاستخراج النوع
أخطاء مخصصة تنفيذ Error() + Unwrap()، تضمين سياق الأعمال
panic/recover فقط للأخطاء غير القابلة للتعافي؛ الالتقاط بـ recover في defer
فلسفة الأخطاء معالجة صريحة، فحص كل خطأ، الأخطاء قيم ليست استثناءات
تغليف الأخطاء استخدم %w للحفاظ على الأخطاء الأساسية، %w لتجاهلها
عادة المجتمع رسائل أخطاء بأحرف صغيرة بدون علامات ترقيم، كقيمة إرجاع أخيرة

ثلاث قواعد ذهبية لمعالجة أخطاء Go:

  1. تحقق فورًا — بعد استلام error، تحقق من err != nil مباشرة
  2. غلف في كل طبقة — أضف السياق في كل طبقة، استخدم %w للحفاظ على الخطأ الأصلي
  3. تنزّل بأناقة — تعافَ إن أمكن، أبلغ للأعلى إن لم يكن، panic كملاذ أخير فقط

📝 تمارين

تمرين 1: تنفيذ قارئ ملف مع إعادة المحاولة

اكتب دالة ReadWithRetry(path string, maxRetries int) ([]byte, error) بهذه المتطلبات:

GO
// الإطار المرجعي
var ErrMaxRetriesExceeded = errors.New("max retries exceeded")

func ReadWithRetry(path string, maxRetries int) ([]byte, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        data, err := os.ReadFile(path)
        if err == nil {
            return data, nil
        }
        lastErr = err
        fmt.Printf("  Retry %d failed: %v\n", i+1, err)
    }
    return nil, fmt.Errorf("reading %s: %w (last error: %v)", path, ErrMaxRetriesExceeded, lastErr)
}

تمرين 2: بناء نظام مستويات أخطاء

نفّذ نظام مستويات أخطاء يدعم مستويات الأخطاء التالية:

GO
type Level int

const (
    LevelDebug Level = iota
    LevelInfo
    LevelWarn
    LevelError
    LevelFatal
)

type LeveledError struct {
    Level   Level
    Message string
    Cause   error
}

المتطلبات:

تمرين 3: تنفيذ مُجمّع أخطاء (آمن للتزامن)

اكتب نوع ErrorCollector لجمع الأخطاء من عدة goroutines في سيناريوهات متزامنة:

GO
type ErrorCollector struct {
    // الحقول التي تحتاجها
}

// Add يُضيف خطأ (آمن للتزامن)
func (c *ErrorCollector) Add(err error) { ... }

// Errors يُرجع جميع الأخطاء المجمعة
func (c *ErrorCollector) Errors() []error { ... }

// HasErrors يتحقق مما إذا كانت هناك أخطاء
func (c *ErrorCollector) HasErrors() bool { ... }

// Error يدمج جميع الأخطاء في خطأ واحد
func (c *ErrorCollector) Error() error { ... }

المتطلبات:


الدرس التالي

بعد إتمام هذا الدرس، تابع مع الدرس 11: الحزم والوحدات لتعلّم كيفية تنظيم كود Go وإدارة التبعيات ونشر حزمك الخاصة.

Web-Tutorial.com

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

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

100%