Ctrl+C في Go: اعتراض SIGINT وتنظيف «defer» تحت الغطاء

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

هل يمكن اعتراض Ctrl+C في Go وتنفيذ تنظيف مثل defer؟ تحليل تحت الغطاء

\n\n

هل سبق لك أن قمت بتشغيل خادم بلغة Go، وضغطت Ctrl+C وتساءلت: «ماذا لو كان بإمكاني إغلاق اتصالات قاعدة البيانات بدقة، أو حفظ ذاكرة التخزين المؤقت، أو ببساطة قول "وداعًا!" قبل الخروج؟» هذا السؤال شائع جدًا على Stack Overflow (آلاف المشاهدات!) لدرجة أننا قررنا تحليله بالتفصيل. اليوم لن نقدم لك وصفة جاهزة فحسب، بل سننظر تحت الغطاء — كيف يعالج Go الإشارات، ما هو signal.Notify ولماذا defer ليس له علاقة كبيرة بالأمر.

\n\n

هل أنت مستعد؟ لنبدأ!

\n\n

المشكلة: Ctrl+C يقتل البرنامج فورًا

\n\n

افتراضيًا، عندما تضغط Ctrl+C في الطرفية، يرسل نظام التشغيل إشارة SIGINT إلى العملية. برنامج Go، إذا لم تفعل شيئًا، سينتهي بشكل مفاجئ — ستتوقف جميع goroutines، ولن يتم تنفيذ دوال defer. هذا مثل نزع القابس من المقبس: سيتم إيقاف تشغيل الكمبيوتر، ولكن قد تُفقد البيانات.

\n\n

مثال كلاسيكي: تكتب خادم ويب، تفتح اتصالاً بـ PostgreSQL، وفجأة — Ctrl+C. إذا لم تعالج الإشارة، سيعلق الاتصال، ولن يتم تحرير مجموعة الاتصالات. بعد بضع عمليات إعادة تشغيل من هذا القبيل، ستقول قاعدة البيانات: «آسف، لا أستطيع أكثر».

\n\n
package main\n\nimport (\n    "fmt"\n    "os"\n    "os/signal"\n    "syscall"\n)\n\nfunc main() {\n    // ... الكود الخاص بك\n    fmt.Println("اضغط Ctrl+C للإنهاء")\n    // انتظار الإشارة\n    sig := make(chan os.Signal, 1)\n    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)\n    <-sig\n    fmt.Println("تم استلام الإشارة، جارٍ الإنهاء...")\n}
\n\n

هذا الكود يلتقط الإشارة بالفعل، ولكن ماذا لو احتجنا إلى تنفيذ تنظيف معقد؟ هنا يبدأ السحر.

\n\n

كيف يعمل هذا تحت الغطاء: signal.Notify والقنوات

\n\n

تقوم الدالة signal.Notify بتسجيل القناة الخاصة بك لاستقبال إشارات معينة. عندما يرسل نظام التشغيل إشارة، يضعها Go runtime في القناة. إذا كانت القناة مخزنة مؤقتًا (وقمنا بإنشاء مخزن مؤقت بحجم 1)، فلن تُفقد الإشارة، حتى لو كانت goroutine المستقبلة مشغولة.

\n\n

ملاحظة مهمة: signal.Notify يعترض الإشارة، لكنه لا يمنع انتشارها. إذا لم تعالج الإشارة في القناة، سيظل البرنامج ينتهي بعد تنفيذ defer (إذا كانت موجودة في main). ولكن إذا كنت تريد تنفيذ تنظيف — عليك انتظار اكتماله.

\n\n

مخطط العمل:

\n
    \n
  • نظام التشغيل → يرسل SIGINT إلى العملية
  • \n
  • Go runtime → يضع الإشارة في القناة المسجلة عبر signal.Notify
  • \n
  • goroutine الخاصة بك → تقرأ من القناة وتشغل التنظيف
  • \n
  • بعد التنظيف → تستدعي os.Exit(0) أو تخرج ببساطة من main
  • \n
\n\n

ولكن كيف نجعل التنظيف «بأسلوب defer»؟ لأن defer يتم تنفيذه عند العودة من الدالة، وليس عند استلام الإشارة. الإجابة: نستخدم defer + حظر main حتى استلام الإشارة.

\n\n

الوصفة: تنظيف «يشبه defer» عند Ctrl+C

\n\n

الفكرة بسيطة: نكتب دوال التنظيف التي يجب تنفيذها عند الإنهاء، ونعلقها على defer. ثم نحظر main حتى استلام الإشارة. عندما تصل الإشارة، تنتهي main، ويتم تفعيل جميع دوال defer. أنيق، أليس كذلك؟

\n\n
package main\n\nimport (\n    "fmt"\n    "os"\n    "os/signal"\n    "syscall"\n    "time"\n)\n\nfunc cleanup() {\n    fmt.Println("التنظيف: جارٍ إغلاق الاتصالات...")\n    time.Sleep(2 * time.Second) // محاكاة العمل\n    fmt.Println("اكتمل التنظيف")\n}\n\nfunc main() {\n    // سيتم تنفيذ defer عند أي إنهاء لـ main\n    defer cleanup()\n\n    // قناة للإشارة\n    sig := make(chan os.Signal, 1)\n    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)\n\n    fmt.Println("الخادم قيد التشغيل. اضغط Ctrl+C للإيقاف.")\n    fmt.Println("جارٍ تنفيذ التنظيف...")\n\n    //                

المدونات

توصيات الكتب