مهلة زمنية في عشرة أسطر
وظيفة context.Context أن يخبر الشيفرة متى تتوقّف. هنا تحصل عملية بطيئة على 50 ms، وتستسلم عندما يطلب السياق ذلك:
ينتهي الاستدعاء الأول في 10 ms ويعيد rows <nil>. الثاني يحتاج 200 ms، لكن السياق ينتهي عند 50 ms (محسوبة من لحظة إنشائه)، فيعيد context deadline exceeded.
لا شيء يُوقَف بالقوة. لا تملك Go طريقة لقتل goroutine من الخارج. السياق إشارة، وعلى الشيفرة أن تتحقّق منها: بالانتظار على ctx.Done() داخل select، أو بفحص ctx.Err() بين الخطوات، أو بتمرير ctx إلى استدعاءات مكتبات (http.NewRequestWithContext وdb.QueryContext وexec.CommandContext) تتحقّق منه نيابة عنك.
الواجهة Context
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
| التابع | ما يعيده |
|---|---|
Done() | قناة تُغلق عند إلغاء السياق أو انقضاء مهلته (nil لسياق لا يمكن إلغاؤه أبدًا) |
Err() | nil ما دام نشطًا، ثم context.Canceled أو context.DeadlineExceeded |
Deadline() | الموعد النهائي وtrue، أو ok == false إن لم يوجد |
Value(key) | القيمة المخزّنة تحت key في هذا السياق أو أحد أسلافه، أو nil |
السياقات غير قابلة للتعديل. لا تغيّر سياقًا أبدًا؛ بل تشتقّ منه ابنًا بإحدى دوال With، فيضيف الابن إشارة إلغاء أو موعدًا نهائيًا أو قيمة.
من أين يأتي السياق
تبدأ كل شجرة سياقات من جذر:
context.Background()فيmainوinitوالاختبارات والإعداد العلوي للخوادم.context.TODO()عندما يجب أن تأخذ الدالة سياقًا لكن المستدعي لا يملكه بعد. يتصرّف تمامًا مثلBackground؛ الاسم علامة لإعادة الهيكلة لاحقًا.
داخل معالج HTTP لا تنشئ جذرًا. بل تستخدم r.Context()، الذي يلغيه الخادم عند انقطاع العميل أو عودة المعالج.
WithCancel: التوقّف عند الطلب
تعيد context.WithCancel سياقًا ابنًا ودالة cancel. استدعاء cancel يغلق قناة Done للابن وقنوات Done لكل ما اشتُقّ منه.
يقع إرسال المنتج داخل select بجوار ctx.Done(). وهذا ما يسمح له بالتوقّف: لو كان out <- i وحده لتوقّف إلى الأبد بعد أن يكفّ المستهلك عن القراءة، ولتسرّبت الـ goroutine. تنتظر for range nums الأخيرة حتى يغلق المنتج القناة. وأثناء التفريغ قد يتمكّن المنتج من إرسال قيمة أو اثنتين، لأن Go تختار حالة عشوائيًا عندما تكون حالتا select جاهزتين؛ الإلغاء سريع لكنه ليس فوريًا.
استدعاء cancel آمن أكثر من مرة ومن أي goroutine. الاستدعاء الأول وحده يفعل شيئًا.
WithTimeout وWithDeadline
WithTimeout(parent, d) تساوي WithDeadline(parent, time.Now().Add(d)). استخدم المهلة لمعنى "على الأكثر هذه المدة"، والموعد النهائي عندما يكون لديك وقت مطلق.
بعد مرور الوقت تُغلق Done وتعيد Err القيمة context.DeadlineExceeded. وإذا استُدعيت cancel أولًا تعيد Err القيمة context.Canceled. تحقّق من أيهما بـ errors.Is، لأن المكتبات تغلّف الخطأ عادة:
استدعِ cancel دائمًا، حتى لمهلة ستنقضي من تلقاء نفسها. يحتفظ السياق بمؤقّت وبموضع لدى أبيه حتى يحدث أحدهما، وdefer cancel() تحرّرهما فور عودة الدالة. يبلغ go vet عن دالة إلغاء مهملة: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak.
الأبناء لا يعيشون أطول من آبائهم
تشكّل السياقات شجرة. إلغاء الأب يلغي كل الأحفاد. قد يكون للابن موعد نهائي أقصر من أبيه، لكن ليس أطول أبدًا: الموعد الأبكر يفوز دائمًا.
هذا ما يجعل السياقات مفيدة عبر الطبقات. يحصل معالج HTTP على سياق يموت مع الطلب؛ واستدعاء قاعدة بيانات بعد ثلاث طبقات يشتقّ منه مهلة ثانيتين. إذا أغلق العميل الاتصال بعد 100 ms، يُلغى الاستعلام حينها، لا بعد ثانيتين.
انتظر دائمًا على ctx.Done() عندما تتوقّف
كل goroutine تنتظر (إرسالًا على قناة، أو استقبالًا، أو مؤقّتًا) يجب أن تنتظر ctx.Done() في الوقت نفسه. أما الحلقات الحسابية التي لا تتوقّف أبدًا، فافحص فيها ctx.Err() بين حين وآخر:
for i, item := range items {
if i%1000 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
process(item)
}
استخدم time.After داخل select للانتظار البسيط، لكن فضّل مؤقّتًا يمكنك إيقافه (أو مهلة سياق) عندما يُلغى الانتظار كثيرًا.
أسباب الإلغاء (Go 1.20 و1.21)
لا تقول ctx.Err() إلا canceled أو deadline exceeded. لتسجيل السبب استخدم صيغ Cause:
وصلت WithCancelCause في Go 1.20، وWithTimeoutCause وWithDeadlineCause في Go 1.21. تستمر Err في إعادة القيم القياسية فتبقى الفحوص الموجودة تعمل؛ وcontext.Cause تعطي التفاصيل.
WithValue، باعتدال
تربط context.WithValue(parent, key, value) قيمة واحدة. وتبحث ctx.Value(key) عنها عبر سلسلة الآباء.
قواعد القيم:
- استخدم نوعًا غير مُصدَّر للمفاتيح، ولا تستخدم
stringعاديًا أبدًا. حزمتان تستخدمان كلتاهما"user"ستكتب إحداهما فوق الأخرى. (لا يكتشفgo vetذلك؛ أماstaticcheckفيكتشفه.) - غلّف الوصول بدوال مساعدة ذات أنواع مثل
WithRequestIDوRequestID، كي لا يرى المستدعونanyولا المفتاح. - خزّن فقط البيانات الخاصة بالطلب التي تمرّ عبر الواجهات البرمجية: معرّفات التتبّع والطلب، والمستخدم الموثَّق، والمسجِّل. لا تخزّن أبدًا المعاملات الاختيارية أو مقابض قواعد البيانات أو الإعدادات. مكانها وسائط الدوال أو حقول البنى، حيث يتحقّق منها المترجم ويراها القرّاء.
- يمرّ البحث على السلسلة أبًا بعد أب، فكل قيمة تضيفها تطيل البحث عن الأخريات خطوة.
الأعراف
ctx context.Contextهو المعامل الأول لكل دالة تجري إدخالًا وإخراجًا، أو تتوقّف، أو تستدعي ما يفعل ذلك:func Fetch(ctx context.Context, url string) error.- لا تخزّن سياقًا في بنية. مرّره إلى كل استدعاء تابع. السياق يخصّ عملية واحدة، والبنية تعيش عادة أطول منها. (الاستثناء نوع يمثّل عملية واحدة، مثل
http.Request.) - لا تمرّر
nilسياقًا أبدًا. استخدمcontext.TODO()إن لم يكن لديك أفضل منه. - أعد
ctx.Err()، أو غلّفها بـ%w، عندما تتوقّف بسبب السياق، ليميّز المستدعون المهلة من فشل حقيقي.
السياق في خوادم HTTP وعملائه
جهة الخادم: يُلغى r.Context() عند انقطاع العميل، أو عودة المعالج، أو إعادة ضبط تدفّق HTTP/2. جهة العميل: تجعل http.NewRequestWithContext الطلب يحترم المهلة أو الإلغاء. يشغّل هذا البرنامج الطرفين عبر httptest:
يستسلم العميل عند 50 ms ويغلق الاتصال. يلاحظ الخادم ذلك، فيُلغى سياق طلبه، ويتوقّف المعالج بدل إنفاق 450 ميلي ثانية أخرى على تقرير لن يقرأه أحد. في معالج حقيقي تمرّر r.Context() إلى كل استدعاء قاعدة بيانات وHTTP، فتتوقّف كلها معًا.
دوال مساعدة أخرى (Go 1.21)
- تعيد
context.WithoutCancel(ctx)سياقًا بالقيم نفسها لا يُلغى عند إلغاءctx. استخدمها لعمل يجب أن يكتمل بعد انتهاء الطلب، مثل كتابة سجل تدقيق. - تشغّل
context.AfterFunc(ctx, f)الدالةfفي goroutine خاصة بها بعد انتهاءctx، وتعيد دالةstopلإلغاء تسجيلها.
أخطاء شائعة
- عدم استدعاء
cancel. اكتب دائمًاdefer cancel()مباشرة بعدWithCancelأوWithTimeoutأوWithDeadline. - تشغيل goroutine تتجاهل
ctx. إذا توقّفت دون الانتظار علىctx.Done()، فالإلغاء لا يفعل شيئًا وتتسرّب الـ goroutine. - إنشاء
context.Background()جديد في عمق سلسلة استدعاءات. يقطع ذلك الصلة بالموعد النهائي للمستدعي وبإلغائه. مرّر الـctxالذي أُعطي لك. - مقارنة الأخطاء بـ
==. استخدمerrors.Is(err, context.DeadlineExceeded)؛ معظم المكتبات تغلّف الخطأ. - استخدام
WithValueللاعتماديات. مقبض قاعدة بيانات مخبّأ في سياق هو معامل لم يعد المترجم قادرًا على التحقّق منه. - توقّع أن يكون الإلغاء فوريًا. لا تلاحظ الشيفرة الإلغاء إلا عند فحصها التالي. الحلقة الطويلة دون فحص تستمر في العمل.
الأسئلة الشائعة
لماذا يُستخدم context في Go؟
يخبر context.Context الدالة وكل ما تستدعيه متى تتوقّف: لأن المستدعي ألغى، أو لأن الموعد النهائي مرّ، أو لأن العميل قطع الاتصال. ويمكنه أيضًا حمل قيم خاصة بالطلب مثل معرّف الطلب. اصطلاحًا يكون المعامل الأول، واسمه ctx.
ما الفرق بين context.Background وcontext.TODO؟
كلاهما يعيد سياقًا فارغًا لا يُلغى أبدًا وليس له موعد نهائي ولا قيم. سلوكهما متطابق. Background() هو الجذر في main والاختبارات والإعداد العلوي. أما TODO() فيعلّم مكانًا يجب أن يُمرَّر إليه سياق حقيقي لكن الشيفرة المحيطة لا تملكه بعد، فيسهل العثور عليه لاحقًا.
لماذا يجب استدعاء cancel بعد context.WithTimeout؟
تسجّل WithTimeout وWithDeadline وWithCancel السياق الجديد لدى أبيه وقد تشغّل مؤقّتًا. استدعاء cancel يحرّر هذه الموارد فور انتهائك، بدل انتظار انقضاء المهلة أو إلغاء الأب. اكتب defer cancel() مباشرة بعد إنشائه؛ ويحذّر go vet عند إهمال دالة الإلغاء.
ماذا تعني رسالة "context deadline exceeded" في Go؟
هي نص context.DeadlineExceeded، الخطأ الذي تعيده ctx.Err() بعد مرور الموعد النهائي للسياق. الدوال التي تحترم السياق، مثل عملاء HTTP ومشغّلات قواعد البيانات، تعيده (مغلّفًا غالبًا) عند نفاد الوقت. تحقّق منه بـ errors.Is(err, context.DeadlineExceeded).
هل أستخدم context.WithValue لتمرير المعاملات؟
لا. استخدمها فقط للبيانات الخاصة بالطلب التي تعبر حدود الواجهات البرمجية ولا تحتاج الدوال الوسيطة إلى معرفتها، مثل معرّف التتبّع أو المستخدم الموثَّق. كل ما تحتاجه الدالة لأداء عملها مكانه معاملاتها، حيث يتحقّق منه المترجم.