Go: سياق Go
آخر تحديث: 2026-08-26
يُعد السياق حجر الزاوية في التحكم في التزامن في لغة Go — فهو يتيح تمرير فترات الانتظار والإلغاءات والقيم بسلاسة عبر سلسلة استدعاءات goroutine.
عندما يتعين على خدمتك التعامل مع عشرات التبعيات الخارجية في آن واحد (لكل منها منطقها الخاص في تحديد مهلة الانتظار والإلغاء)، كيف يمكنك إدارتها بطريقة متسقة؟ في هذا الدرس، ستتعلم كيفية استخدام حزمة context في لغة Go بشكل شامل.
1. ستتعلم
context.Background()وcontext.TODO()context.WithCancelالإلغاء اليدويcontext.WithTimeoutيتم إلغاؤه تلقائيًا بعد انقضاء مهلة الانتظارcontext.WithDeadlineإلغاء الموعد النهائيcontext.WithValue: تمرير القيم على مستوى الطلب- السياق: قواعد تمرير الكرة المتسلسلة
- تجربة عملية: منع حدوث انقضاءات الوقت المتتالية في سلاسل الخدمات الصغيرة
2. قصة حقيقية لمهندس برمجيات الخلفية
(1) المشكلة: إذا انتهت مهلة أحد المكونات في المرحلة الأولية، يتعطل النظام بأكمله
شياولي هي مهندسة تعمل على نظام «order». وهي مسؤولة عن واجهة برمجة تطبيقات REST تعتمد على ثلاث خدمات تابعة:
"استدعت واجهة برمجة التطبيقات (API) الخاصة بـ order ثلاث خدمات: خدمة المخزون، وخدمة الدفع، وخدمة الإشعارات. وفي أحد الأيام، استغرقت خدمة المخزون 20 ثانية للرد، مما تسبب في ارتفاع استهلاك الذاكرة في خدمتي بشكل حاد، حيث كانت جميع goroutines في انتظارها — كما لم تتمكن طلبات المستخدمين الآخرين من الوصول أيضًا. فسألني مديري: «لماذا صفحة order معطلة تمامًا؟»"
تحليل المشكلة:
// 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) حل لعبة «غو»: السياق
// 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
}
}
الإخراج (عادي):
Order placed successfully
(3) الفوائد: مع السياق مقابل بدون سياق
| الحالة | النتيجة |
|---|---|
| عدم التحكم في مهلة الانتظار | تسرب في الغوروتين، نفاد الذاكرة في النظام |
| وقت يدوي. بعد إجراء الفحوصات | الكود غير منظم؛ لكل دالة مهلة انتظار خاصة بها |
| التحكم الموحد في السياقات | في حالة إلغاء السياق الأصلي، يتم إلغاء جميع السياقات الفرعية بشكل متتالي |
3. سياق الجذر
(1) الخلفية وقائمة المهام
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: الإلغاء اليدوي
▶ مثال: إلغاء «جوروتين» يدويًّا
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)
}
▶ مثال: الإلغاء التسلسلي
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)
}
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: الإلغاء التلقائي عند انتهاء المهلة
▶ مثال: التحكم في مهلة الانتظار
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
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
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. قواعد تسلسل السياقات
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. مثال كامل: التحكم في مهلة الانتظار لتتبع الخدمات الصغيرة
// 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) لتحديد نوع الإلغاء الذي حدث واتخاذ قرار بشأن استراتيجية إعادة المحاولة وفقًا لذلك.
❓ أسئلة شائعة
Background() وTODO()؟Background() هي السياق الجذري الذي تستخدمه حاليًا في كودك؛ TODO() هو عنصر مؤقت، يشير إلى أن هذا الجزء من الكود لم يتم دمجه بعد في السياق ويجب إعادة هيكلته في أقرب وقت ممكن.WithTimeout أم WithDeadline؟WithTimeout (فعبارة «انتظر حتى ثانيتين» أكثر بديهية). استخدم WithDeadline فقط عندما تكون مهلة الانتظار عبارة عن وقت محدد («يجب الانتهاء بحلول الساعة 15:30:00»). WithTimeout تستدعي داخليًّا WithDeadline.Context عدة مرات؟cancel() أي خطر (فالدعوة الثانية وما يليها لا تؤدي إلى أي تأثير). ولن تتلقى جميع goroutines التي تستمع إلى Done() الإشارة إلا مرة واحدة.WithValue؟context.WithValue غير قابلة للتعديل. وما يُشار إليه بـ«التعديل» ينطوي في الأساس على إنشاء سياق جديد. ولا تؤثر قيمة WithValue في السياق الفرعي على السياق الأصلي. وهذا يضمن أمان التزامن.📖 ملخص
- Background() / TODO(): نوعان من سياقات الجذر؛ لا يمكن إلغاؤهما
- WithCancel: التحكم يدويًّا في إلغاء الجوروتين
- WithTimeout / WithDeadline: الإلغاء التلقائي عند انتهاء المهلة
- WithValue: تمرير القيم على مستوى الطلب (أنواع المفاتيح المخصصة)
- تمرير السلسلة: الحجة الأولى هي دائمًا Context
- الإلغاء التسلسلي: إلغاء العنصر الأصلي → إلغاء جميع العناصر التابعة
- يجب استدعاء
cancel()باستخدامdefer - تجربة عملية: منع حدوث انقضاءات الوقت المتتالية في سلاسل الخدمات الصغيرة
📝 تمارين
-
تمرين أساسي (مستوى الصعوبة ⭐): اكتب دالة
FetchWithTimeout(ctx, url string, timeout time.Duration)تستخدمcontext.WithTimeoutللتحكم في مهلة انتظار طلبات HTTP. وبمجرد انتهاء المهلة، يجب إلغاء طلب HTTP الأساسي تلقائيًا. -
مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ مدير مهام مجدولة قابلة للإلغاء بشكل متزامن. يجب أن يدعم ما يلي: (1) تسجيل مهام مجدولة متعددة (مهمة واحدة لكل goroutine)؛ (2) الإلغاء الفردي (عن طريق استدعاء الدالة
cancel)؛ (3) الإلغاء الجماعي (تسلسل WithCancel)؛ (4) تشترك جميع المهام في سياق جذري واحد. -
التحدي (الصعوبة ⭐⭐⭐): قم بمحاكاة نظام التتبع الموزع. المتطلبات: (1) تمرير معرّف التتبع (TraceID) باستخدام
WithValueعبر ثلاث طبقات من استدعاءات الدوال (API → الخدمة → قاعدة البيانات)؛ (2) لكل طبقة تحكمها الخاص بمهلة الانتظار (الخدمة: 2 ثانية، قاعدة البيانات: 500 مللي ثانية)؛ (3) في حالة انتهاء مهلة الانتظار، يتم إلغاء الطبقة الحالية دون التأثير على الطبقات الأعلى؛ (4) إخراج الوقت المنقضي وTraceID لكل مرحلة. استخدمDeadline()منContextلحساب الوقت المتبقي.