الإرسال والاستقبال
القناة (channel) أنبوب ذو نوع محدّد بين الـ goroutines. ch <- v ترسل، و<-ch تستقبل. أنشئ القناة بـ make:
القيمة الصفرية لنوع القناة هي nil، لذا فإن var ch chan string دون make تعطيك قناة تتوقّف إلى الأبد. أنشئ القنوات دائمًا بـ make.
القنوات غير المخزّنة تُزامِن
تنشئ make(chan T) قناة غير مخزّنة. الإرسال يتوقّف حتى يأخذ مستقبلٌ القيمة، والاستقبال يتوقّف حتى يقدّمها مرسلٌ. تلتقي الـ goroutine عند هذه النقطة، وهذا يجعل القناة غير المخزّنة أداة تزامن بقدر ما هي أنبوب بيانات. عندما تعود <-done في المثال التالي، تعرف أن العامل أنهى كل ما قبل الإرسال:
قراءة result في main آمنة هنا دون mutex. يضمن نموذج الذاكرة في Go أن كل ما فعله العامل قبل الإرسال يصبح مرئيًا لـ main بعد الاستقبال المقابل. النوع chan struct{} هو النوع المعتاد للإشارة المجرّدة، لأن struct{} لا يشغل أي ذاكرة.
القنوات المخزّنة
تعطي make(chan T, n) القناة مساحة لـ n قيمة. ينجح الإرسال دون مستقبل حتى يمتلئ المخزن، وينجح الاستقبال حتى يفرغ. تخرج القيم بالترتيب الذي دخلت به.
المخزن يفصل المرسل عن المستقبل حتى لا توقف الدفعات القصيرة المرسل. لكنه لا يعالج منتجًا أسرع دائمًا من مستهلكه؛ هو فقط يؤخّر اللحظة التي يتوقّف فيها المرسل. اختر حجم المخزن لسبب (عدد المرسلين، حجم دفعة معروف)، لا لإخفاء جمود.
len(ch) لقطة لحظية. قبل أن تتصرّف بناءً عليها قد تكون goroutine أخرى غيّرتها، فلا تستخدمها لتقرّر هل سيتوقّف الإرسال. استخدم لذلك select مع حالة default.
الإغلاق وrange
تخبر close(ch) المستقبلين بأن لا قيم أخرى ستُرسل. بعد الإغلاق:
- القيم الموجودة في المخزن ما زالت تُسلَّم،
- بعدها يعيد كل استقبال القيمة الصفرية فورًا،
- تُبلغ
v, ok := <-chأنok == false، - تنتهي
for v := range ch.
الاستقبال الأول بعد close ما زال يحصل على "last" مع ok == true. والثاني يحصل على القيمة الصفرية "" وfalse.
قواعد تسبّب panic:
- الإرسال على قناة مغلقة يسبّب panic برسالة
send on closed channel، - إغلاق قناة مغلقة مسبقًا يسبّب panic،
- إغلاق قناة
nilيسبّب panic.
لذلك يغلق طرف الإرسال فقط، ومرة واحدة. مع عدة مرسلين لا يعرف أي منهم متى انتهى الآخرون؛ اجعل goroutine منفصلة تنتظر كل المرسلين (بـ sync.WaitGroup) وتغلق القناة بعد أن تعود Wait. الإغلاق مطلوب فقط عندما ينتظر مستقبلٌ النهاية. القناة غير المغلقة التي لا يشير إليها شيء يجمعها جامع النفايات مثل أي قيمة أخرى.
أنواع الاتجاه
يمكن للدالة أن تصرّح بأنها ترسل فقط أو تستقبل فقط على قناة، فيرفض المترجم العملية الأخرى.
| النوع | المعنى | المسموح |
|---|---|---|
chan T | ثنائية الاتجاه | إرسال، استقبال، إغلاق |
chan<- T | إرسال فقط | إرسال، إغلاق |
<-chan T | استقبال فقط | استقبال |
يتحوّل chan T ضمنيًا إلى أي من النوعين المقيّدين عند تمريره إلى دالة. لهذا تستطيع produce في الأعلى أن تعيد <-chan int: يمكن للمستدعين المرور عليها بـ range لكن لا يمكنهم الإرسال إليها ولا إغلاقها. استخدم أنواع الاتجاه في كل معامل دالة يناسبها ذلك، فهي توثّق الملكية وتحوّل سوء الاستخدام إلى خطأ ترجمة.
الجمود (Deadlock)
إذا توقّفت كل goroutine ولم يعد هناك ما يوقظ أيًا منها، يُنهي وقت التشغيل البرنامج:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/main.go:7 +0x38
exit status 2
يخبرك تفريغ الـ goroutines بالعملية العالقة (chan send هنا، أو chan receive أو sync.WaitGroup.Wait أو select). الأسباب المعتادة:
- إرسال على قناة غير مخزّنة دون مستقبل يعمل،
rangeعلى قناة لا تُغلق أبدًا،- عدّاد
WaitGroupلا يصل إلى الصفر أبدًا، - goroutine تنتظر كل منهما الأخرى.
لا يكتشف وقت التشغيل إلا الحالة التي تعلق فيها كل الـ goroutines. في خادم فيه goroutines أخرى حيّة (مستمع HTTP، أو ticker) لا يسبّب الخطأ نفسه أي انهيار؛ الـ goroutine العالقة تتسرّب فقط.
القنوات nil
الإرسال والاستقبال على قناة nil يتوقّفان إلى الأبد. يبدو ذلك بلا فائدة لكنه حيلة معروفة داخل select: تعيين متغيّر قناة إلى nil يعطّل حالتها. المثال التالي يدمج قناتين ويتوقّف عن الاستماع إلى كل منهما بعد إغلاقها:
دون تعيينات nil تكون القناة المغلقة جاهزة دائمًا، وستدور الحلقة على قيم صفرية.
خط معالجة (pipeline)
تتركّب القنوات في خطوط معالجة: كل مرحلة goroutine تستقبل من قناة وترسل إلى التالية، وتغلق مخرجها عندما ينتهي مدخلها.
كل مرحلة تعمل بالتزامن، وترتيب المخرجات ثابت (1، 16، 81) لأن كل مرحلة goroutine واحدة تحافظ على الترتيب. الإغلاقات تتتابع: generate تغلق، فتنتهي حلقة range في square، فتغلق مخرجها، وهكذا حتى main.
نقطة الضعف في هذا الخط: لو توقّفت main عن القراءة مبكرًا، لتوقّفت المراحل على عمليات الإرسال إلى الأبد. خطوط المعالجة الحقيقية تأخذ context.Context أو قناة done وتستخدم select عليها بجوار كل إرسال.
قناة أم mutex
القنوات لنقل ملكية البيانات والإشارة إلى الأحداث. أما sync.Mutex فأبسط لحماية حالة مشتركة تقرؤها وتحدّثها goroutines كثيرة في مكانها، مثل ذاكرة تخزين مؤقت أو عدّاد. بنية بداخلها mutex أوضح غالبًا من goroutine تملك الحالة وتخدم الطلبات عبر القنوات. استخدم ما يجعل الشيفرة أقصر والملكية واضحة.
مرجع سريع
| العملية | قناة nil | قناة مفتوحة | قناة مغلقة |
|---|---|---|---|
ch <- v | تتوقّف إلى الأبد | تتوقّف حتى الاستقبال أو وجود مساحة في المخزن | panic |
<-ch | تتوقّف إلى الأبد | تتوقّف حتى تتوفّر قيمة | القيم المخزّنة، ثم القيمة الصفرية |
v, ok := <-ch | تتوقّف إلى الأبد | ok صحيحة | ok خاطئة بعد التفريغ |
close(ch) | panic | تُغلق | panic |
len(ch)، cap(ch) | 0، 0 | القيم المخزّنة، حجم المخزن | القيم الباقية، حجم المخزن |
الأسئلة الشائعة
ما الفرق بين القناة المخزّنة وغير المخزّنة في Go؟
القناة غير المخزّنة (make(chan int)) لا تملك مساحة تخزين: الإرسال يتوقّف حتى تستقبل goroutine أخرى، فكل إرسال هو تسليم ونقطة تزامن في الوقت نفسه. أما القناة المخزّنة (make(chan int, 3)) فتحتفظ بما يصل إلى 3 قيم؛ الإرسال يتوقّف فقط عندما يمتلئ المخزن، والاستقبال يتوقّف فقط عندما يفرغ.
ماذا يحدث عند القراءة من قناة مغلقة في Go؟
الاستقبال من قناة مغلقة لا يتوقّف أبدًا. يفرّغ أولًا القيم الباقية في المخزن، ثم يعيد القيمة الصفرية لنوع العنصر إلى الأبد. استخدم v, ok := <-ch للتمييز: تصبح ok مساوية لـ false بعد أن تُغلق القناة وتفرغ. وتتوقّف حلقة for v := range ch عند هذه النقطة.
من يجب أن يغلق القناة في Go؟
المُرسِل، وفقط عندما يحتاج المستقبلون إلى معرفة أن لا مزيد من القيم (مثلًا لإنهاء حلقة range). الإرسال على قناة مغلقة يسبّب panic، وكذلك إغلاق القناة مرتين، لذا فإغلاق المستقبل للقناة يتسابق مع المرسلين. لست مضطرًا لإغلاق القناة لتحريرها؛ جامع النفايات يستعيد القنوات التي لا يصل إليها شيء في الحالتين.
ماذا تعني رسالة "fatal error: all goroutines are asleep"؟
كل goroutine في البرنامج متوقّفة على عملية قناة أو قفل لا يمكن لأي شيء أن يكملها، فيوقف وقت التشغيل البرنامج. السبب الأشيع هو الإرسال على قناة غير مخزّنة في main دون goroutine أخرى تستقبل، أو المرور بـ range على قناة لا تُغلق أبدًا.