Go: سياق Go

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

يُعد السياق حجر الزاوية في التحكم في التزامن في لغة Go — فهو يتيح تمرير فترات الانتظار والإلغاءات والقيم بسلاسة عبر سلسلة استدعاءات goroutine.

عندما يتعين على خدمتك التعامل مع عشرات التبعيات الخارجية في آن واحد (لكل منها منطقها الخاص في تحديد مهلة الانتظار والإلغاء)، كيف يمكنك إدارتها بطريقة متسقة؟ في هذا الدرس، ستتعلم كيفية استخدام حزمة context في لغة Go بشكل شامل.

1. ستتعلم



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

(1) المشكلة: إذا انتهت مهلة أحد المكونات في المرحلة الأولية، يتعطل النظام بأكمله

شياولي هي مهندسة تعمل على نظام «order». وهي مسؤولة عن واجهة برمجة تطبيقات REST تعتمد على ثلاث خدمات تابعة:

"استدعت واجهة برمجة التطبيقات (API) الخاصة بـ order ثلاث خدمات: خدمة المخزون، وخدمة الدفع، وخدمة الإشعارات. وفي أحد الأيام، استغرقت خدمة المخزون 20 ثانية للرد، مما تسبب في ارتفاع استهلاك الذاكرة في خدمتي بشكل حاد، حيث كانت جميع goroutines في انتظارها — كما لم تتمكن طلبات المستخدمين الآخرين من الوصول أيضًا. فسألني مديري: «لماذا صفحة order معطلة تمامًا؟»"

تحليل المشكلة:

GO
// Bad code: no timeout control
func PlaceOrder(ctx context.Context, order Order) error {
    // If InventoryCheck hangs for 30 seconds, the goroutine waits needlessly for 30 seconds
    ok, err := InventoryCheck(ctx, order.Items)
    if err != nil {
        return err
    }
    // If the payment service times out, 30 seconds have already been wasted; the user has long since given up
    err = Charge(ctx, order.Total)
    if err != nil {
        return err
    }
    // Notification service also hangs... goroutine leak reaches the limit → OOM
    return Notify(ctx, order.UserID)
}

(2) حل لعبة «غو»: السياق

GO
// context_demo.go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // Root Context
    root := context.Background()

    // Wrap with WithTimeout: 2-second timeout
    ctx, cancel := context.WithTimeout(root, 2*time.Second)
    defer cancel() // Ensure resources are released

    result := PlaceOrder(ctx, "الترتيب-123")
    fmt.Println(result)
}

func PlaceOrder(ctx context.Context, orderID string) string {
    // Check if Context is already canceled
    الاختيار {
    case <-ctx.Done():
        return fmt.Sprintf("Canceled: %v", ctx.Err())
    default:
    }

    // Assign a shorter timeout to each downstream call
    checkCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
    defer cancel()

    ok := InventoryCheck(checkCtx, orderID)
    if !ok {
        return "Insufficient inventory"
    }
    return "Order placed successfully"
}

func InventoryCheck(ctx context.Context, orderID string) bool {
    // Simulate a slow call
    الاختيار {
    case <-time.After(500 * time.Millisecond):
        return true
    case <-ctx.Done():
        fmt.Printf("InventoryCheck canceled: %v\n", ctx.Err())
        return false
    }
}

الإخراج (عادي):

TEXT 📖 للعرض فقط
Order placed successfully

(3) الفوائد: مع السياق مقابل بدون سياق

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


3. سياق الجذر

(1) الخلفية وقائمة المهام

GO
package main

import (
    "context"
    "fmt"
)

func main() {
    // Background(): root Context, never canceled
    // Used in main functions, initialization, top-level requests
    ctx := context.Background()
    fmt.Printf("Background: %v\n", ctx)

    // TODO(): placeholder when unsure which Context to use
    // Marks code that has not yet been integrated with Context and needs refactoring
    todo := context.TODO()
    fmt.Printf("TODO: %v\n", todo)
}

(2) الخلفية مقابل قائمة المهام

السياق الغرض هل سيتم إلغاؤه؟
Background() العقدة الجذرية، نقطة البداية لجميع السياقات أبدًا
TODO() علامة مؤقتة تشير إلى أن الكود لم يتم دمجه بعد مع Context أبدًا
💡 نصيحة: context.Background() هي العقدة الجذرية لجميع أشجار السياق ولا يتم إلغاؤها أبدًا. تُستخدم context.TODO() لتمييز الكود الذي لم يتم دمجه بعد في سلسلة السياق — يجب عليك استبدالها بسياق مناسب في أقرب وقت ممكن.



4. context.WithCancel: الإلغاء اليدوي

▶ مثال: إلغاء «جوروتين» يدويًّا

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func Worker(ctx context.Context, id int) {
    for {
        الاختيار {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go Worker(ctx, 1)
    go Worker(ctx, 2)

    time.Sleep(2 * time.Second)
    fmt.Println("Main goroutine initiating cancellation...")
    cancel() // Tell all Workers to stop

    // Wait for goroutines to exit
    time.Sleep(500 * time.Millisecond)
}
▶ جرّب الكود

▶ مثال: الإلغاء التسلسلي

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func handler(ctx context.Context) {
    // Child Context inherits parent Context
    childCtx, cancel := context.WithCancel(ctx)
    defer cancel()

    go subTask(childCtx, "task-1")
    go subTask(childCtx, "task-2")

    // Parent Context canceled → Child Context automatically canceled
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("Handler complete")
    case <-ctx.Done():
        fmt.Println("Handler canceled")
    }
}

func subTask(ctx context.Context, name string) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Printf("%s complete\n", name)
    case <-ctx.Done():
        fmt.Printf("%s canceled: %v\n", name, ctx.Err())
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go handler(ctx)

    time.Sleep(500 * time.Millisecond)
    cancel() // Cancel → handler → subTask all cascadingly canceled

    time.Sleep(1 * time.Second)
}
▶ جرّب الكود
100%
sequenceDiagram
    participant Main as main()
    participant H as handler
    participant ST1 as subTask-1
    participant ST2 as subTask-2

    Main->>H: WithCancel
    H->>ST1: WithCancel
    H->>ST2: WithCancel
    Note over Main,ST2: Normal execution
    Main->>Main: cancel()
    Main-->>H: ctx.Done()
    H-->>ST1: ctx.Done()
    H-->>ST2: ctx.Done()
    Note over Main,ST2: All cascadingly canceled
🔥 خطأ شائع: يجب استدعاء cancel(). حتى إذا استخدمت WithTimeout، يجب عليك تأجيل استدعاء cancel(). وإلا، فلن يتم تحرير موارد السياق (المؤقتات، والغوروتينات). القاعدة: عند إنشاء WithCancel أو WithTimeout أو WithDeadline → قم على الفور باستدعاء defer cancel().



5. context.WithTimeout: الإلغاء التلقائي عند انتهاء المهلة

▶ مثال: التحكم في مهلة الانتظار

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func callExternalAPI(ctx context.Context, name string, delay time.Duration) (string, error) {
    select {
    case <-time.After(delay):
        return fmt.Sprintf("%s response", name), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // 1-second timeout
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    // Call two downstream services
    result1 := make(chan string, 1)
    result2 := make(chan string, 1)

    go func() {
        r, err := callExternalAPI(ctx, "ServiceA", 500*time.Millisecond)
        if err != nil {
            result1 <- fmt.Sprintf("ServiceA failed: %v", err)
            return
        }
        result1 <- r
    }()

    go func() {
        r, err := callExternalAPI(ctx, "ServiceB", 1500*time.Millisecond)
        if err != nil {
            result2 <- fmt.Sprintf("ServiceB failed: %v", err)
            return
        }
        result2 <- r
    }()

    fmt.Println(<-result1) // ServiceA response (500ms < 1s timeout)
    fmt.Println(<-result2) // ServiceB failed: context deadline exceeded (1500ms > 1s)
}
▶ جرّب الكود

▶ مثال: WithTimeout مقابل WithDeadline

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // WithTimeout: timeout 2 seconds from now
    timeoutCtx, cancel1 := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel1()

    // WithDeadline: specify absolute time
    deadline := time.Now().Add(2 * time.Second)
    deadlineCtx, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()

    // Both have the same effect
    fmt.Printf("timeoutCtx deadline: %v\n", timeoutCtx.Deadline())
    fmt.Printf("deadlineCtx deadline: %v\n", deadlineCtx.Deadline())
}
▶ جرّب الكود

(3) WithTimeout مقابل WithDeadline

الطريقة المعلمات الغرض
WithTimeout(parent, 2*time.Second) الوقت النسبي الأكثر استخدامًا: «انتظر لمدة تصل إلى ثانيتين»
WithDeadline(parent, time.Time) الوقت المطلق يحدد موعدًا نهائيًا: «يجب الانتهاء بحلول الساعة 15:30»


6. تمرير القيم على مستوى الطلب باستخدام context.WithValue

▶ مثال: WithValue

GO
package main

import (
    "context"
    "fmt"
)

// Custom key type (avoids conflicts)
type contextKey string

const (
    UserIDKey    contextKey = "user_id"
    TraceIDKey   contextKey = "trace_id"
    RequestIDKey contextKey = "request_id"
)

func middleware(ctx context.Context) context.Context {
    // Get trace ID from request header
    ctx = context.WithValue(ctx, TraceIDKey, "trace-123")
    ctx = context.WithValue(ctx, RequestIDKey, "req-456")
    return ctx
}

func handler(ctx context.Context, userID string) {
    ctx = context.WithValue(ctx, UserIDKey, userID)

    // Pass to business layer
    service(ctx)
}

func service(ctx context.Context) {
    // Retrieve values from Context
    userID := ctx.Value(UserIDKey).(string)
    traceID := ctx.Value(TraceIDKey).(string)
    requestID := ctx.Value(RequestIDKey).(string)

    fmt.Printf("Processing request: user=%s, trace=%s, request=%s\n",
        userID, traceID, requestID)
}

func main() {
    ctx := context.Background()
    ctx = middleware(ctx)
    handler(ctx, "user-007")
}
▶ جرّب الكود
🔥 خطأ شائع: يجب أن يكون key في context.WithValue نوعًا مخصصًا؛ فلا يمكنك استخدام سلسلة أحرف مباشرةً. فإذا استخدمت حزمتان السلسلة "user_id" كمفتاح، فسيحدث تعارض. يُعد استخدام النوع المخصص type contextKey string من أفضل الممارسات.

(2) حالات استخدام WithValue

السيناريو موصى به غير موصى به
TraceID / RequestID ✅ تم تمريرها عبر WithValue ❌ متغير عام
رمز المصادقة ✅ تم تمريره بواسطة WithValue ❌ معلمة الدالة
اتصال قاعدة البيانات ❌ تم الحصول عليه عبر حقن التبعية ❌ WithValue
معلمة الأعمال ❌ معلمة صريحة ❌ التمرير الضمني باستخدام WithValue


7. قواعد تسلسل السياقات

GO
package main

import (
    "context"
    "fmt"
    "time"
)

type contextKey string

func main() {
    root := context.Background()

    // Chaining: WithCancel → WithTimeout → WithValue
    ctx1, cancel1 := context.WithCancel(root)
    defer cancel1()

    ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second)
    defer cancel2()

    ctx3 := context.WithValue(ctx2, contextKey("trace"), "trace-007")

    // ctx3 inherits ctx1's cancellation + ctx2's timeout + ctx3's value
    fmt.Printf("ctx3 deadline: %v\n", ctx3.Deadline())
    fmt.Printf("ctx3 value: %v\n", ctx3.Value(contextKey("trace")))

    // Cancel ctx1 first → ctx2 and ctx3 both receive the cancellation signal
    cancel1()
    time.Sleep(10 * time.Millisecond)
    fmt.Printf("ctx2 err: %v\n", ctx2.Err())
    fmt.Printf("ctx3 err: %v\n", ctx3.Err())
}

(1) قواعد تمرير السياق

القاعدة الوصف
المعلمة الأولى المعلمة الأولى في توقيع الدالة هي دائمًا ctx context.Context
لا تقم بتخزينه في بنية لا تقم بتخزين Context في حقل بنية؛ بل قم بتمريره كمعلمة
التمرير بين الدوال تتلقى كل دالة تحتاج إلى معرفة حالات الإلغاء أو انتهاء المهلة سياقًا
سلسلة ثابتة كل استدعاء لـ WithCancel/WithTimeout/WithValue يُرجع سياقًا جديدًا
الإلغاء التسلسلي عند إلغاء عنصر رئيسي → يتم إلغاء جميع العناصر التابعة؛ أما إلغاء عنصر تابع فلا يؤثر على العنصر الرئيسي


8. مثال كامل: التحكم في مهلة الانتظار لتتبع الخدمات الصغيرة

GO
// microservice_chain.go
package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

// ---------- Simulated downstream services ----------

// Inventory service
type InventoryService struct{}

func (s *InventoryService) Check(ctx context.Context, orderID string) (bool, error) {
    // Simulate random delay
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return true, nil
    case <-ctx.Done():
        return false, ctx.Err()
    }
}

// Payment service
type PaymentService struct{}

func (s *PaymentService) Charge(ctx context.Context, amount float64) (string, error) {
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return "pay-" + fmt.Sprintf("%d", time.Now().UnixNano()), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

// Notification service
type NotificationService struct{}

func (s *NotificationService) Send(ctx context.Context, userID, message string) error {
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

// ---------- Business layer ----------

type OrderService struct {
    inventory *InventoryService
    payment   *PaymentService
    notify    *NotificationService
}

func NewOrderService() *OrderService {
    return &OrderService{
        inventory: &InventoryService{},
        payment:   &PaymentService{},
        notify:    &NotificationService{},
    }
}

// PlaceOrder uses Context to control the entire chain's timeout
func (s *OrderService) PlaceOrder(ctx context.Context, userID, orderID string, amount float64) error {
    // 1. Inventory check (wait at most 1 second)
    invCtx, invCancel := context.WithTimeout(ctx, 1*time.Second)
    defer invCancel()

    ok, err := s.inventory.Check(invCtx, orderID)
    if err != nil {
        return fmt.Errorf("inventory check failed: %w", err)
    }
    if !ok {
        return fmt.Errorf("insufficient inventory")
    }

    // 2. Payment (wait at most 2 seconds)
    payCtx, payCancel := context.WithTimeout(ctx, 2*time.Second)
    defer payCancel()

    paymentID, err := s.payment.Charge(payCtx, amount)
    if err != nil {
        return fmt.Errorf("payment failed: %w", err)
    }

    // 3. Notification (wait at most 500ms)
    notifyCtx, notifyCancel := context.WithTimeout(ctx, 500*time.Millisecond)
    defer notifyCancel()

    err = s.notify.Send(notifyCtx, userID, "Order placed: "+orderID)
    if err != nil {
        // Notification failure does not affect the order (async log recording)
        fmt.Printf("Notification failed (logged): %v\n", err)
    }

    fmt.Printf("Order placed successfully: user=%s, order=%s, payment=%s\n", userID, orderID, paymentID)
    return nil
}

// ---------- Client ----------

func main() {
    svc := NewOrderService()

    // Overall request timeout of 3 seconds
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    err := svc.PlaceOrder(ctx, "user-007", "order-123", 99.99)
    if err != nil {
        fmt.Printf("Order failed: %v\n", err)
    }
}
💡 نصيحة: تُرجع طريقة Err() الخاصة بـ Context إحدى قيمتين محتملتين: context.Canceled (تم الإلغاء يدويًّا) أو context.DeadlineExceeded (انتهت مهلة الانتظار). داخل السلسلة، يمكنك استخدام errors.Is(err, context.DeadlineExceeded) لتحديد نوع الإلغاء الذي حدث واتخاذ قرار بشأن استراتيجية إعادة المحاولة وفقًا لذلك.



❓ أسئلة شائعة

س متى ينبغي استخدام Context؟
ج في أي سيناريو يتطلب التحكم في مهلة الانتظار، أو الإلغاء اليدوي، أو تمرير القيم على مستوى الطلب. ويشمل ذلك: معالجة طلبات HTTP، واستعلامات قواعد البيانات، ومكالمات RPC، والمهام المجدولة. لا تستخدم Context في الدوال الحسابية البحتة التي لا تتطلب الإلغاء أو التحكم في مهلة الانتظار.
س ما الفرق بين Background() وTODO()؟
ج كلاهما سياقات جذرية لا يمكن إلغاؤها أبدًا، لكن لكل منهما معنى مختلف: Background() هي السياق الجذري الذي تستخدمه حاليًا في كودك؛ TODO() هو عنصر مؤقت، يشير إلى أن هذا الجزء من الكود لم يتم دمجه بعد في السياق ويجب إعادة هيكلته في أقرب وقت ممكن.
س أيهما يجب أن أختار، WithTimeout أم WithDeadline؟
ج في معظم الحالات، استخدم WithTimeout (فعبارة «انتظر حتى ثانيتين» أكثر بديهية). استخدم WithDeadline فقط عندما تكون مهلة الانتظار عبارة عن وقت محدد («يجب الانتهاء بحلول الساعة 15:30:00»). WithTimeout تستدعي داخليًّا WithDeadline.
س هل يمكن إلغاء Context عدة مرات؟
ج لا يمكن تشغيل عملية الإلغاء إلا مرة واحدة — ولا تشكل الاستدعاءات المتكررة لـ cancel() أي خطر (فالدعوة الثانية وما يليها لا تؤدي إلى أي تأثير). ولن تتلقى جميع goroutines التي تستمع إلى Done() الإشارة إلا مرة واحدة.
س ماذا يحدث عندما يحدث الإلغاء من قبل السياق الأصلي وانتهاء المهلة في الوقت نفسه؟
ج يتم تشغيل كلا الأمرين بشكل مستقل، ويكون للأمر الذي يحدث أولاً هو الذي يسري مفعوله. إذا تم إلغاء السياق الأصلي يدويًّا، تتلقى جميع السياقات الفرعية إشارة Done() على الفور، حتى لو لم تنتهِ بعد مهلة الانتظار الخاصة بالسياقات الفرعية. وهذا يضمن أقصر مسار لعمليات الإلغاء المتتالية.
س هل يمكن تعديل القيمة التي يتم تمريرها إلى WithValue؟
ج لا. قيمة context.WithValue غير قابلة للتعديل. وما يُشار إليه بـ«التعديل» ينطوي في الأساس على إنشاء سياق جديد. ولا تؤثر قيمة WithValue في السياق الفرعي على السياق الأصلي. وهذا يضمن أمان التزامن.
س ما أنواع البيانات التي يُعد Context مناسبًا لتخزينها؟
ج إنه مناسب فقط لتخزين البيانات الوصفية على مستوى الطلب: TraceID وRequestID وUserID ورموز المصادقة. وهو غير مناسب لتخزين المعلمات التجارية (مثل السعر أو الكمية)، كما أنه غير مناسب لتخزين اتصالات قواعد البيانات أو التكوينات — حيث يجب تمرير هذه العناصر عبر حقن التبعية.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (مستوى الصعوبة ⭐): اكتب دالة FetchWithTimeout(ctx, url string, timeout time.Duration) تستخدم context.WithTimeout للتحكم في مهلة انتظار طلبات HTTP. وبمجرد انتهاء المهلة، يجب إلغاء طلب HTTP الأساسي تلقائيًا.

  2. مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ مدير مهام مجدولة قابلة للإلغاء بشكل متزامن. يجب أن يدعم ما يلي: (1) تسجيل مهام مجدولة متعددة (مهمة واحدة لكل goroutine)؛ (2) الإلغاء الفردي (عن طريق استدعاء الدالة cancel)؛ (3) الإلغاء الجماعي (تسلسل WithCancel)؛ (4) تشترك جميع المهام في سياق جذري واحد.

  3. التحدي (الصعوبة ⭐⭐⭐): قم بمحاكاة نظام التتبع الموزع. المتطلبات: (1) تمرير معرّف التتبع (TraceID) باستخدام WithValue عبر ثلاث طبقات من استدعاءات الدوال (API → الخدمة → قاعدة البيانات)؛ (2) لكل طبقة تحكمها الخاص بمهلة الانتظار (الخدمة: 2 ثانية، قاعدة البيانات: 500 مللي ثانية)؛ (3) في حالة انتهاء مهلة الانتظار، يتم إلغاء الطبقة الحالية دون التأثير على الطبقات الأعلى؛ (4) إخراج الوقت المنقضي وTraceID لكل مرحلة. استخدم Deadline() من Context لحساب الوقت المتبقي.

Web-Tutorial.com

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

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

100%