انتظار عدة قنوات
تبدو select مثل switch، لكن كل حالة فيها إرسال أو استقبال على قناة. تتوقّف حتى تستطيع حالة ما المضي ثم تنفّذها.
مع هذه التأخيرات تحصل select الأولى على النتيجة السريعة والثانية على البطيئة. لا يحتاج أي استقبال إلى انتظار القناة الأخرى. أما <-slow ثم <-fast فستعالجهما بترتيب ثابت أيًا كانت التي وصلت أولًا.
كيف تُقيَّم select:
- تُقيَّم كل تعابير القنوات والقيم المراد إرسالها مرة واحدة، بترتيب الشيفرة، عند بدء
select. - إذا جهزت حالة أو أكثر، تُختار إحداها عشوائيًا.
- إذا لم تجهز أي حالة ووُجدت
default، تُنفَّذdefault. - وإلا تتوقّف الـ goroutine حتى تجهز حالة ما.
select {} الفارغة تتوقّف إلى الأبد. تراها أحيانًا في نهاية main في برامج يجري عملها الحقيقي في goroutines أخرى.
الاختيار العشوائي بين الحالات الجاهزة
عندما تجهز عدة حالات في الوقت نفسه، لا تفضّل select الأولى في الترتيب. يملأ هذا البرنامج قناتين مخزّنتين ثم ينفّذ select ألف مرة:
يتغيّر التوزيع بين countA وcountB في كل تشغيل ويقترب من 500 لكل منهما. الاختيار العشوائي مقصود: يمنع قناة مزدحمة من تجويع الأخريات. إذا احتجت أولوية فانظر النمط لاحقًا.
عمليات دون توقّف مع default
مع حالة default لا تتوقّف select أبدًا. وهذا يحوّل الإرسال أو الاستقبال إلى عملية "محاولة":
إسقاط العمل عندما يمتلئ المخزن هو طريقة تخفيف الحمل أو إرسال المقاييس دون إيقاف المستدعي أبدًا.
لا تضع default في select داخل حلقة for لمجرّد "فحص" القنوات مرارًا. عندما لا يكون شيء جاهزًا تدور الحلقة مستهلكة 100% من المعالج. توقّف بدلًا من ذلك، وأضف حالة مهلة إن احتجت الاستيقاظ دوريًا.
المهل
تعيد time.After(d) قناة تستقبل مرة واحدة بعد d. سابقها مع العمل الحقيقي:
يعيد الاستدعاء الأول "data". ويعيد الثاني خطأ المهلة بعد 50 ms، قبل أن ينتهي العامل بوقت طويل. لقناة النتيجة مخزن بحجم 1 عن قصد. عندما تفوز المهلة لا يستقبل أحد من result أبدًا؛ ومع قناة غير مخزّنة كانت goroutine العامل ستتوقّف على إرسالها إلى الأبد وتتسرّب.
في الحلقة تنشئ time.After مؤقّتًا جديدًا في كل تكرار، وهذا صحيح تمامًا لمهلة خمول لكل رسالة ("لا رسالة منذ ثانية"). أما لموعد نهائي كلي عبر عمليات كثيرة، فأنشئ مؤقّتًا أو سياقًا واحدًا قبل الحلقة. منذ Go 1.23 تُجمع المؤقّتات التي لم يعد يشير إليها شيء حتى لو لم تنطلق، فلم تعد time.After في حلقة تحتفظ بالذاكرة حتى ينطلق كل مؤقّت، كما كانت تفعل في الإصدارات الأقدم (يحتاج هذا go 1.23 أو أحدث في go.mod).
حلقات for-select وقنوات الإيقاف
الـ goroutine التي تعمل حتى يُطلب منها التوقّف هي حلقة for حول select فيها حالة للعمل وحالة للتوقّف:
إغلاق quit بدل الإرسال عليها هو الأسلوب المعتاد: يراه كل مستقبل، الآن ولاحقًا، فتوقف close واحدة أي عدد من العمّال. وتتيح قناة done لـ main الانتظار حتى يعود العامل فعلًا.
في الشيفرة الحقيقية تكون قناة الإيقاف عادة context.Context: case <-ctx.Done():. تعمل بالطريقة نفسها (تعيد Done() قناة تُغلق عند الإلغاء) وتحمل أيضًا المواعيد النهائية وسبب التوقّف. صفحة context تشرحها.
break داخل select
تخرج break في حالة من حالات select من select، لا من for المحيطة. هذا مصدر شائع لحلقات لا تنتهي. استخدم return، أو ضع وسمًا على الحلقة:
الأولوية بين القنوات
لأن select تختار عشوائيًا، لا يمكنك ترتيب الحالات داخل عبارة واحدة. لتجعل قناة تفوز كلما كان فيها شيء، افحصها أولًا وحدها:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
تعود select الأولى فورًا إذا كان الإلغاء قد حدث. ومن دونها قد يستمر تدفّق ثابت من المهام في الفوز بالاختيار العشوائي لفترة بعد إلغاء ctx.
القنوات nil تعطّل الحالة
الإرسال أو الاستقبال على قناة nil لا يجهز أبدًا، فالحالة على قناة nil معطّلة فعليًا. تعيين مدخل مغلق إلى nil هو طريقة التوقّف عن انتظاره مع متابعة الأخرى؛ وتعرض صفحة القنوات حلقة دمج مبنية بهذه الطريقة. والحيلة نفسها تشغّل المهلة وتطفئها: أبقِ var timeout <-chan time.Time مساوية لـ nil حتى تحتاجها، ثم أسند إليها time.After(d).
أخطاء شائعة
- توقّع ترتيب الشيفرة. الحالة الأولى في الترتيب لا تُفضَّل.
- حلقة مشغولة مع
default.for { select { ... default: } }دون عمل تستهلك نواة معالج. - تسريب الخاسر. عندما تفوز المهلة، يجب أن تبقى الـ goroutine التي كانت سترسل النتيجة قادرة على الانتهاء. أعطِ قناتها مخزنًا بحجم 1.
breakتخرج منselectوحدها. استخدم وسمًا أوreturn.time.Afterواحدة لكل تكرار تُستخدم كموعد نهائي كلي. تبدأ من جديد في كل تكرار؛ أنشئ الموعد النهائي مرة واحدة خارج الحلقة.
الأسئلة الشائعة
ماذا تفعل select في Go؟
تنتظر select حتى تستطيع إحدى عمليات القنوات فيها (إرسال أو استقبال) المضي، ثم تنفّذ تلك الحالة. إذا جهزت عدة حالات في الوقت نفسه تختار واحدة عشوائيًا. دون حالة default تتوقّف حتى تجهز حالة ما؛ ومع default لا تتوقّف أبدًا.
كيف أضيف مهلة إلى الاستقبال من قناة في Go؟
ضع الاستقبال ومؤقّتًا في select واحدة: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. ما يحدث أولًا يفوز. داخل حلقة، أو عندما يملك المستدعي موعدًا نهائيًا أصلًا، استخدم context.Context مع context.WithTimeout وانتظر على ctx.Done() بدلًا من ذلك.
هل تختار select في Go الحالات بالترتيب؟
لا. عندما تجهز أكثر من حالة تختار Go عشوائيًا بالتساوي، فلا تستطيع حالة تجويع الأخريات. إذا احتجت أولوية، فافحص القناة ذات الأولوية العالية في select خاصة بها مع default أولًا، ثم انتقل إلى select على كل القنوات.
لماذا لا تخرجني break من حلقة for-select؟
داخل select تخرج break من عبارة select وحدها، لا من for المحيطة. استخدم return، أو ضع وسمًا على الحلقة (loop: for { select { case <-done: break loop } }).