Go runtime يراقب كل الـ goroutines بالبرنامج، ولو لاحظ أن كل واحدة منها متوقّفة بانتظار شيء لن يحدث أبدًا، يوقف البرنامج فورًا بدل تعليقه للأبد:
fatal error: all goroutines are asleep - deadlock!
هذه ليست panic عادية تلتقطها بـ recover — هي قرار من الـ runtime نفسه
بإيقاف كل شيء لأنه لا يرى أي أمل بالتقدّم.
السبب الجذري دائمًا واحد
كل الـ goroutines — بما فيها main — عالقة بانتظار عملية قناة أو قفل
مزامنة لن يُكمله أي طرف آخر. أشهر ثلاث صور لهذا:
1) قناة بلا مستقبل (أو بلا مرسل)
القناة غير المخزَّنة (unbuffered) تحجب المرسل حتى يستقبل طرف آخر القيمة مباشرة. لو ما في أحد يستقبل، الإرسال يعلّق للأبد:
func main() {
ch := make(chan int)
ch <- 42 // ينتظر مستقبِلًا... لا يوجد أحد
fmt.Println("لن نصل هنا أبدًا")
}
الحل: شغّل الاستقبال بـ goroutine منفصلة، أو استخدم قناة مخزَّنة إن كنت تعرف عدد القيم مسبقًا:
ch := make(chan int, 1) // مخزَّنة بسعة 1 — لا تحتاج مستقبِلًا فوريًا
ch <- 42
fmt.Println(<-ch)
2) انتظار بلا goroutine ثانية أصلًا
استقبال من قناة داخل نفس الـ goroutine الوحيدة بالبرنامج بلا أي مصدر آخر يملأها:
func main() {
ch := make(chan string)
msg := <-ch // ننتظر قيمة... ما في غيرنا ليرسلها
fmt.Println(msg)
}
الحل: تأكّد أن كل قناة تنتظر منها قيمة لها مصدر فعلي — عادة go
تشغّل الدالة التي ترسل:
ch := make(chan string)
go func() {
ch <- "جاهز"
}()
fmt.Println(<-ch)
3) مزامنة لن تكتمل — WaitGroup أو Mutex
sync.WaitGroup ينتظر wg.Done() بعدد يساوي wg.Add() بالضبط. لو نُسي
Done بأحد المسارات (مثلًا داخل شرط أو مسار خطأ)، wg.Wait() ينتظر
للأبد:
var wg sync.WaitGroup
wg.Add(1)
go func() {
// defer wg.Done() نُسيت هنا — الانتظار لن ينتهي أبدًا
doWork()
}()
wg.Wait()
الحل: ضع defer wg.Done() أول سطر داخل الـ goroutine مباشرة، قبل أي
منطق قد يتفرّع أو يفشل مبكرًا:
wg.Add(1)
go func() {
defer wg.Done() // يُنفَّذ دائمًا مهما كان مسار الخروج
doWork()
}()
wg.Wait()
نفس المبدأ ينطبق على mu.Lock() بلا mu.Unlock() مقابل بمسار خروج
معيّن — القفل يبقى محجوزًا وأي goroutine أخرى تنتظره تتجمّد.
لماذا يظهر أحيانًا فقط؟
لأن الجمود يعتمد على ترتيب تشغيل الـ goroutines، وGo لا يضمن نفس الترتيب بكل تشغيلة. قد يعمل كودك بالصدفة عدّة مرّات قبل أن يظهر الخطأ — وهذا بالضبط ما يجعله خطيرًا بالإنتاج لو اختبرته بسرعة فقط محليًا.
خطوات التشخيص السريعة
- لكل
ch <- vأو<-chبالكود، اسأل: من الطرف الآخر فعليًا وهل هو يعمل بالتأكيد؟ - لكل
wg.Add(n)، تأكّد أنwg.Done()يُنفَّذ بالضبط n مرّة مهما كان مسار الخروج —deferأول الدالة أضمن مكان. - شغّل
go run -race main.go— يكشف أيضًا سباقات البيانات المرتبطة غالبًا بنفس الكود المعطوب.
الخلاصة
all goroutines are asleep - deadlock! رسالة واضحة النية: كل شيء
متوقّف بانتظار طرف لن يصل. تتبّع كل قناة ومزامنة بالكود واسأل من
المفترض يحرّكها من الطرف الآخر — الحل يكون قريبًا دائمًا.
تعمّق أكثر بدرسي القنوات (Channels) والمزامنة بـ sync ضمن مسار Go الكامل بالعربي.