Go: معالجة الأخطاء وإدارة الحزم في Go

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

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

تُعد فلسفة معالجة الأخطاء في لغة Go وإدارة الحزم ركائز أساسية للكود المخصص للاستخدام في بيئات الإنتاج. في هذا الدرس، ستتقن أهم قدرتين أساسيتين في لغة Go يتم التقليل من شأنهما بشكل كبير.

1. ستتعلم



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

(1) المشكلة: تسبب الذعر الذي انتشر عبر الإنترنت في تعطل الخدمة، وامتلأت مجموعة التنبيهات برسائل الخطأ 500

أليس هي مهندسة برمجيات خلفية في فريق الخدمات الصغيرة. وقد واجهت خدمة المستخدم التي تتولى صيانتها مؤخرًا مشكلة كبيرة:

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

قامت بفتح رمز الخطأ في موقع العطل:

GO
// Bad code: No error handling; it just panics.
func getUserByID(db *sql.DB, id int) *User {
    rows, _ := db.Query("SELECT * FROM users WHERE id = ?", id)
    // If the ID does not exist, rows.Next() returns false.
    // However, directly accessing the value below—the rows.Scan operation on nil—causes a panic.
    var user User
    for rows.Next() {
        rows.Scan(&user.Name, &user.Age)
    }
    return &user
}

ثلاث مشكلات: (1) يتم تجاهل الأخطاء في db.Query؛ (2) لا يتم التحقق من وجود النتيجة؛ (3) يؤدي حدوث حالة ذعر إلى تعطل العملية بأكملها.

(2) حل لعبة «غو»: الأخطاء هي قيم

GO
// user_service.go
package main

import (
    "errors"
    "fmt"
)

// Custom Error Types
type NotFoundError struct {
    ID int
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("user %d not found", e.ID)
}

// Error Sentinel
var ErrInvalidInput = errors.New("invalid input")

// Robust Query Function
func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("findUser: %w", ErrInvalidInput)
    }

    users := map[int]User{
        1: {Name: "Alice", Age: 28},
        2: {Name: "Bob", Age: 32},
    }

    user, ok := users[id]
    if !ok {
        return nil, NotFoundError{ID: id}
    }
    return &user, nil
}

type User struct {
    Name string
    Age  int
}

func main() {
    for _, id := range []int{1, -1, 999} {
        user, err := findUser(id)
        if err != nil {
            // Determining the Type of Error
            if errors.Is(err, ErrInvalidInput) {
                fmt.Printf("Input error (skipped): %v\n", err)
                continue
            }
            var nf NotFoundError
            if errors.As(err, &nf) {
                fmt.Printf("User %d does not exist\n", nf.ID)
                continue
            }
            fmt.Printf("Unknown error: %v\n", err)
            continue
        }
        fmt.Printf("Found: %s (%d)\n", user.Name, user.Age)
    }
}

الناتج:

TEXT 📖 للعرض فقط
Found: Alice (28)
Input error (skipped): findUser: invalid input
User 999 does not exist

(3) المزايا: مقارنة بين طرق معالجة الأخطاء

البعد لغة try-catch خطأ Go
الخطأ هو تدفق التحكم في الاستثناءات قيمة الإرجاع العادية
صريح ضمني (من السهل أن يفوتك كتلة catch) صريح if err != nil
الأداء عبء فك التكديس لا يوجد عبء إضافي
قابلية التركيب ضعيفة (تؤدي إلى مقاطعة غير طبيعية للتدفق) جيدة (يمكن تمرير err بحرية)
💡 نصيحة: عندما يتم إلقاء استثناءات في لغات Java أو C++ أو Python، تحدث عملية فك المكدس؛ أما في لغة Go، فإن error هي مجرد قيمة واجهة (16 بايت)، لذا فإن تمريرها لا ينطوي عمليًّا على أي عبء إضافي.



3. واجهة الأخطاء

(1) ما هو الخطأ؟

GO
type error interface {
    Error() string
}

أي نوع يُنفِّذ الطريقة Error() string يُعتبر خطأً.

▶ مثال: 4 طرق لإنشاء خطأ

GO
package main

import (
    "errors"
    "fmt"
)

// Method 1: errors.New (most commonly used)
var ErrNotFound = errors.New("resource not found")

// Method 2: fmt.Errorf (with formatting)
func validate(age int) error {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be >= 0)", age)
    }
    return nil
}

// Method 3: Wrap the error in fmt.Errorf (%w)
func loadConfig(path string) error {
    if path == "" {
        return fmt.Errorf("loadConfig: %w", ErrNotFound)
    }
    return nil
}

// Method 4: Customizing the error type
type TimeoutError struct {
    DurationMs int
    Operation  string
}

func (e TimeoutError) Error() string {
    return fmt.Sprintf("%s timed out after %dms", e.Operation, e.DurationMs)
}

func main() {
    // Method 1
    fmt.Println(ErrNotFound)  // resource not found

    // Method 2
    fmt.Println(validate(-5))  // invalid age: -5 (must be >= 0)

    // Method 3
    fmt.Println(loadConfig(""))  // loadConfig: resource not found

    // Method 4
    err := TimeoutError{DurationMs: 5000, Operation: "DB query"}
    fmt.Println(err)  // DB query timed out after 5000ms
}
▶ جرّب الكود

(3) مقارنة بين طرق الإنشاء الأربع

الطريقة الوظيفة/الصيغة الغرض هل تدعم تسلسل الأخطاء؟
errors.New errors.New("msg") خطأ ثابت بسيط
fmt.Errorf fmt.Errorf("msg %d", n) خطأ في التنسيق
fmt.Errorf(%w) fmt.Errorf("ctx: %w", err) خطأ ملفوف ✅ errors.Is/As
النوع المخصص struct { ... Error() string } خطأ في الحقول الإضافية ✅ مخصص


4. سلسلة الأخطاء errors.Is / errors.As

(1) errors.Is: يتحقق مما إذا كان حارس معين مدرجًا في سلسلة الأخطاء

GO
package main

import (
    "errors"
    "fmt"
)

var ErrDB = errors.New("database error")
var ErrConn = fmt.Errorf("connection failed: %w", ErrDB)

func main() {
    err := fmt.Errorf("query failed: %w", ErrConn)

    // errors.Is searches layer by layer along the %w chain
    fmt.Println(errors.Is(err, ErrDB))    // true
    fmt.Println(errors.Is(err, ErrConn))  // true

    // == Can only match the outermost level
    fmt.Println(err == ErrDB)   // false (different objects)
    fmt.Println(err == ErrConn) // false
}

▶ مثال: errors.As: يستخرج أنواعًا محددة من الأخطاء من السلسلة

GO
package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field string
    Value interface{}
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s = %v", e.Field, e.Value)
}

func process(input string) error {
    if input == "" {
        return ValidationError{Field: "input", Value: ""}
    }
    return nil
}

func main() {
    err := process("")

    // errors.As: Extracts the ValidationError type from the chain
    var valErr ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("Field %s is invalid, value=%v\n", valErr.Field, valErr.Value)
    }

    // Also works with wrapping
    wrapped := fmt.Errorf("process failed: %w", err)
    var valErr2 ValidationError
    if errors.As(wrapped, &valErr2) {
        fmt.Printf("(After wrapping) Field %s is invalid\n", valErr2.Field)
    }
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
Field input is invalid, value=
(After wrapping) Field input is invalid

(3) errors.Is مقابل errors.As

الوظيفة طريقة المطابقة الغرض
errors.Is(err, target) يساوي (==) يتحقق مما إذا كان خطأ حراسة معين قد حدث أم لا
errors.As(err, &target) مطابقة الأنواع استرداد خطأ من نوع معين من سلسلة الأخطاء


5. حالة الذعر / التعافي

(1) حالة الذعر: خطأ لا يمكن إصلاحه

GO
package main

import "fmt"

func main() {
    fmt.Println("Start")

    // A panic immediately terminates the current function and begins stack unwinding.
    panic("something went terribly wrong")

    // This line will not be executed
    fmt.Println("End")
}

الناتج:

TEXT 📖 للعرض فقط
Start
panic: something went terribly wrong

goroutine 1 [running]:
main.main()
        /tmp/main.go:8 +0x...
exit status 2

▶ مثال: recover (التعافي من حالة الذعر)

GO
package main

import (
    "fmt"
)

// recover is only useful in defer
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()

    // Intentionally triggering a panic
    if b == 0 {
        panic("division by zero")
    }
    return a / b, nil
}

func main() {
    // Normal call
    if r, err := safeDivide(10, 2); err == nil {
        fmt.Printf("10/2 = %d\n", r)
    }

    // A panic is caught by recover and does not cause a crash
    if r, err := safeDivide(10, 0); err != nil {
        fmt.Printf("Error: %v (result=%d)\n", err, r)
    }

    fmt.Println("Program ended normally—panic was recovered")
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
10/2 = 5
Error: panic recovered: division by zero (result=0)
Program ended normally—panic was recovered

(3) حالات استخدام «panic» مقابل «error»

السيناريو استخدام error استخدام panic
خطأ في الإدخال من قبل المستخدم
الملف غير موجود
انتهت مهلة الشبكة
إشارة مؤشر نيل ❌ (لا يمكن إصلاحها) ✅ (خطأ في الكود)
مؤشر المصفوفة خارج النطاق ❌ (لا يتحقق منه المُجمِّع) ✅ (خطأ في الكود)
فشل التهيئة (شرط إلزامي)
🔥 خطأ شائع: لا ينبغي استخدام panic + recover لمحاكاة كتلة try-catch. تتمثل فلسفة لغة Go في «الاستخدام المقتصد لـ panic، والاستخدام المتكرر لـ error». يجب استخدام panic فقط في حالات الاستثناء الحقيقية (أخطاء في الكود، أو فشل في التهيئة، أو حالات لا يمكن استردادها).



6. إدارة حزم Go Mod

(1) الأوامر الثلاثة الرئيسية في Go Mod

الأمر الوظيفة حالات الاستخدام الشائعة
go mod init <module> تهيئة وحدة بدء مشروع جديد
go mod tidy تنظيف التبعيات (إضافة التبعيات المفقودة، وإزالة التبعيات غير الضرورية) بعد تعديل عبارات الاستيراد
go mod add <path>@<ver> إضافة تبعيات (ميزة جديدة في Go 1.22+) لإضافة حزمة خارجية
go get <path>@<ver> إضافة/تحديث التبعيات الطريقة التقليدية

▶ مثال: إنشاء وحدة نمطية + إضافة التبعيات

BASH
# 1. Initialize module
$ go mod init github.com/alice/user-service
go: creating new go.mod: module github.com/alice/user-service

# 2. Import an external package in the code
GO
package main

import (
    "fmt"
    "github.com/google/uuid"  // external dependency
)

func main() {
    id := uuid.New()
    fmt.Printf("Generated UUID: %s\n", id)
}
BASH
# 3. Add dependencies and organize
$ go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.6.0

# 4. View the generated go.mod
$ cat go.mod
module github.com/alice/user-service

go 1.22

require github.com/google/uuid v1.6.0

(3) قواعد تصدير الحزم

GO
// math/calculator.go
package math

// Uppercase = Public (accessible to other packages)
func Add(a, b int) int { return a + b }
var Version = "1.0"

// Lowercase first letter = private (visible only within the package)
func helper(x int) int { return x * 2 }
var internalVersion = "0.5"

// Public Structure
type Calculator struct {
    // Public field
    Name string
    // Private field (cannot be accessed directly from outside the package)
    precision int
}
GO
package main

import "yourmodule/math"

func main() {
    math.Add(1, 2)      // ✅ Public
    math.Version        // ✅ Public variable

    // math.helper(5)   // ❌ Private function; compilation error
    // math.internalVersion  // ❌ Private variable

    c := math.Calculator{Name: "basic"}  // ✅ Public struct
    // c.precision = 2  // ❌ Private field; compilation error
}

▶ مثال: تصدير الحزمة + تمرير نوع الخطأ

GO
// apperrors/errors.go
package apperrors

import "fmt"

// Public Error Type (Uppercase)
type BusinessError struct {
    Code    int
    Message string
}

func (e BusinessError) Error() string {
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// Public Sentinel
var ErrUnauthorized = BusinessError{Code: 401, Message: "unauthorized"}

// Private error (external packages cannot reference directly)
type internalError struct {
    detail string
}

func (e internalError) Error() string {
    return fmt.Sprintf("internal: %s", e.detail)
}

// Public factory function (external packages use internalError indirectly through this function)
func NewInternalError(detail string) error {
    return internalError{detail: detail}
}
▶ جرّب الكود

7. مثال كامل: خدمة مستخدم متينة

الربط بين معالجة الأخطاء وإدارة الحزم والأخطاء المخصصة:

GO
// user_service.go
package main

import (
    "errors"
    "fmt"
)

// ---------- Error Definitions ----------

type NotFoundError struct {
    Resource string
    ID       int
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
}

type ValidationError struct {
    Field   string
    Message string
}

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

type DBError struct {
    Operation string
    Err       error
}

func (e DBError) Error() string {
    return fmt.Sprintf("db %s failed: %v", e.Operation, e.Err)
}

func (e DBError) Unwrap() error {
    return e.Err
}

// Sentinel Error
var ErrInternal = errors.New("internal server error")

// ---------- Data Layer (Simulated DB) ----------

type User struct {
    ID   int
    Name string
    Age  int
}

func queryUserFromDB(id int) (*User, error) {
    db := map[int]User{
        1: {ID: 1, Name: "Alice", Age: 28},
        2: {ID: 2, Name: "Bob", Age: 32},
    }
    user, ok := db[id]
    if !ok {
        return nil, NotFoundError{Resource: "user", ID: id}
    }
    return &user, nil
}

// ---------- Service Layer ----------

func GetUser(id int) (*User, error) {
    // panic protection
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("[PANIC] recovered: %v\n", r)
        }
    }()

    if id <= 0 {
        return nil, ValidationError{
            Field:   "id",
            Message: "must be positive",
        }
    }

    user, err := queryUserFromDB(id)
    if err != nil {
        var nf NotFoundError
        if errors.As(err, &nf) {
            return nil, nf
        }
        return nil, DBError{
            Operation: "queryUserFromDB",
            Err:       err,
        }
    }

    if user.Age < 0 || user.Age > 150 {
        return nil, ValidationError{
            Field:   "age",
            Message: fmt.Sprintf("unexpected age: %d", user.Age),
        }
    }

    return user, nil
}

// ---------- HTTP Layer ----------

func HandleGetUser(id int) {
    user, err := GetUser(id)
    if err != nil {
        var nf NotFoundError
        var ve ValidationError
        var de DBError

        switch {
        case errors.As(err, &nf):
            fmt.Printf("[404] %v\n", err)
        case errors.As(err, &ve):
            fmt.Printf("[400] %v\n", err)
        case errors.As(err, &de):
            fmt.Printf("[500] db error: %v\n", de)
            fmt.Printf("[500] Internal: %+v\n", de.Err)
        default:
            fmt.Printf("[500] %v\n", err)
        }
        return
    }
    fmt.Printf("[200] User: %+v\n", user)
}

func main() {
    // Normal
    HandleGetUser(1)

    // Input error (ValidationError with additional information)
    HandleGetUser(0)

    // User does not exist (custom NotFoundError)
    HandleGetUser(999)

    fmt.Println("\n=== Program Exited Normally ===")
}

النتيجة المتوقعة:

TEXT 📖 للعرض فقط
[200] User: &{ID:1 Name:Alice Age:28}
[400] validation failed: id - must be positive
[404] user with id 999 not found

=== Program Exited Normally ===
100%
flowchart TD
    A[Function returns error] --> B{err == nil?}
    B -->|Yes| C[Normal processing]
    B -->|No| D[Determine error type]
    D --> E[errors.Is / == sentinel]
    D --> F[errors.As / type assertion]
    D --> G[type switch]
    E --> H[Handle specific sentinel error]
    F --> I[Extract structured error info]
    G --> J[Branch by type]
    H --> K[Return or retry]
    I --> K
    J --> K
🔥 خطأ شائع: تُعد طريقة Unwrap() error في البنية DBError عاملاً أساسياً للسماح للأخطاء المخصصة بالمشاركة في سلسلة الأخطاء. وإذا كان النوع المخصص لا يحتوي على طريقة Unwrap()، فإن errors.Is وerrors.As ستقومان بالتحقق من الطبقة الخارجية فقط.



❓ أسئلة شائعة

س ما هو نوع error؟
ج error هي واجهة مدمجة: type error interface { Error() string }. أي نوع ينفذ الطريقة Error() string يُعتبر error — وهي قيمة واجهة مكونة من 16 بايت.
س كيف يمكنني تخصيص الأخطاء؟
ج قم بتعريف بنية (struct) وقم بتنفيذ الطريقة Error() string. إذا كنت ترغب في دعم تسلسل الأخطاء (التجول errors.Is/As)، أضف طريقة Unwrap() error التي تُرجع الخطأ الداخلي.
س هل من الضروري استخدام recover بعد panic؟
ج ليس بالضرورة. recover لا يكون مفيدًا إلا داخل كتلة defer، ويجب وضعه فقط عند مدخل goroutine (go func() { defer recover() }). لا تستخدم recover في منطق العمل الخاص بك — فذلك يخفي الأخطاء بدلاً من إصلاحها.
س ما الفرق بين errors.Is وerrors.As؟
ج يقوم errors.Is(err, target) بإجراء مقارنات القيم على طول سلسلة %w، مستوىً تلو الآخر (==)؛ بينما يقوم errors.As(err, &target) بإجراء عمليات التحقق من النوع خطوة بخطوة على طول السلسلة ويملأ target. ببساطة: Is يتحقق من القيمة، بينما As يستخرج النوع.
س كيف يدير go mod التبعيات؟
ج سير العمل الأساسي: go mod init للتهيئة → كتابة الكود واستخدام importgo mod tidy للتنزيل والتنظيم تلقائيًا → تثبيت الإصدارات باستخدام go.mod وgo.sum. يقدم Go 1.22+ الأمر go mod add، وهو أكثر سهولة في الاستخدام.
س ما هي القواعد المتعلقة بالأسماء العامة والخاصة؟
ج هناك قاعدة واحدة: الأسماء التي تبدأ بحرف كبير تُعتبر عامة (مُصدَّرة)، بينما تلك التي تبدأ بحرف صغير تُعتبر خاصة. وينطبق هذا على المتغيرات، والدوال، والأنواع، وحقول البنى، والثوابت. ولا توجد كلمات رئيسية public/private.
س ما الفرق بين fmt.Errorf(%w) وfmt.Errorf(%v)؟
ج %w تُنشئ خطأً يتضمن سلسلة أخطاء يمكن لـ errors.Is/As تتبعها؛ أما %v فتقوم ببساطة بتنسيق سلسلة نصية وإنشاء خطأ جديد لا علاقة له بالخطأ الأصلي.
س كيف يمكن معالجة الأخطاء بشكل سلس في كود الإنتاج؟
ج (1) استخدم التداخل fmt.Errorf("context: %w", err) للحفاظ على سلسلة الأخطاء؛ (2) حدد أنواع أخطاء الأعمال باستخدام حقول إضافية؛ (3) قم بمعالجة الأخطاء بشكل موحد على مستوى معالج HTTP → رموز حالة HTTP؛ (4) قم بتسجيل السلسلة الكاملة (%+v).

📖 ملخص


📝 تمارين

  1. المسألة الأساسية (الصعوبة ⭐): عرّف دالة Divide func Divide(a, b float64) (float64, error) التي تُرجع errors.New("division by zero") إذا كان القاسم يساوي 0، وتُرجع الناتج في الحالات الأخرى.

  2. مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ ConfigLoader الذي يدعم تحميل الإعدادات من ملف JSON واللجوء إلى متغيرات البيئة في حالة الفشل. المتطلبات: قم بتغليف كل مستوى خطأ بعلامة fmt.Errorf(%w)، واسمح للمستدعي باستخدام errors.Is لتحديد ما إذا كان الخطأ هو «ملف غير موجود» أم «خطأ في تحليل JSON».

  3. مشكلة التحدي (الصعوبة ⭐⭐⭐): قم ببناء بنية من ثلاث طبقات لمعالجة الأخطاء: (1) طبقة البيانات Repository → تُرجع NotFoundError / DBError؛ (2) طبقة الخدمة Service → تغلف أخطاء طبقة البيانات + تضيف ValidationError؛ (3) معالج HTTP → تحليل الأخطاء طبقةً طبقةً باستخدام errors.As وربطها برموز حالة HTTP (404/400/500). يجب أن تتضمن بنية الخطأ الحقول الخاصة بالأعمال (ID/الحقل/العملية).

Web-Tutorial.com

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

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

100%