هذا مقتطف من التدوينة؛ المنشور الكامل متاح هنا: Golang Defer: من Basic To Trap.
ربما يكون بيان التأجيل أحد الأشياء الأولى التي نجدها مثيرة للاهتمام عندما نبدأ في تعلم Go، أليس كذلك؟
ولكن هناك الكثير مما يزعج العديد من الأشخاص، وهناك العديد من الجوانب الرائعة التي غالبًا ما لا نتطرق إليها عند استخدامه.
على سبيل المثال، تحتوي عبارة التأجيل فعليًا على 3 أنواع (اعتبارًا من Go 1.22، على الرغم من أن ذلك قد يتغير لاحقًا): تأجيل مشفر مفتوح، وتأجيل مخصص للكومة، وتأجيل مخصص للمكدس. يتمتع كل واحد بأداء مختلف وسيناريوهات مختلفة حيث يتم استخدامه بشكل أفضل، وهو أمر جيد لمعرفة ما إذا كنت تريد تحسين الأداء.
في هذه المناقشة، سنغطي كل شيء بدءًا من الأساسيات وحتى الاستخدام الأكثر تقدمًا، وسنتعمق قليلاً في بعض التفاصيل الداخلية.
دعونا نلقي نظرة سريعة على التأجيل قبل أن نتعمق أكثر.
في Go، التأجيل هو كلمة أساسية تستخدم لتأخير تنفيذ وظيفة حتى تنتهي الوظيفة المحيطة.
func main() { defer fmt.Println("hello") fmt.Println("world") } // Output: // world // hello
في هذا المقتطف، تقوم عبارة التأجيل بجدولة fmt.Println("hello") ليتم تنفيذها في نهاية الوظيفة الرئيسية. لذلك، يتم استدعاء fmt.Println("world") على الفور، وتتم طباعة "world" أولاً. بعد ذلك، لأننا استخدمنا التأجيل، تتم طباعة "hello" كخطوة أخيرة قبل التشطيبات الرئيسية.
يشبه الأمر إعداد مهمة ليتم تشغيلها لاحقًا، مباشرة قبل خروج الوظيفة. يعد هذا مفيدًا حقًا لإجراءات التنظيف، مثل إغلاق اتصال قاعدة البيانات، أو تحرير كائن المزامنة (mutex)، أو إغلاق ملف:
func doSomething() error { f, err := os.Open("phuong-secrets.txt") if err != nil { return err } defer f.Close() // ... }
يعد الكود أعلاه مثالًا جيدًا لإظهار كيفية عمل التأجيل، ولكنه أيضًا طريقة سيئة لاستخدام التأجيل. سنتناول ذلك في القسم التالي.
"حسنًا، جيد، ولكن لماذا لا تضع f.Close() في النهاية؟"
هناك عدة أسباب وجيهة لذلك:
عند حدوث حالة من الذعر، يتم فك المكدس ويتم تنفيذ الوظائف المؤجلة بترتيب معين، وهو ما سنغطيه في القسم التالي.
عند استخدام عبارات تأجيل متعددة في إحدى الوظائف، يتم تنفيذها بترتيب "مكدس"، مما يعني أن آخر وظيفة مؤجلة يتم تنفيذها أولاً.
func main() { defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) } // Output: // 3 // 2 // 1
في كل مرة تقوم فيها باستدعاء بيان التأجيل، فإنك تضيف هذه الوظيفة إلى أعلى القائمة المرتبطة لـ goroutine الحالي، مثل هذا:
وعندما تعود الدالة، فإنها تمر عبر القائمة المرتبطة وتنفذ كل واحدة منها بالترتيب الموضح في الصورة أعلاه.
لكن تذكر، أنها لا تنفذ كل التأجيل في قائمة goroutine المرتبطة، بل تقوم فقط بتشغيل التأجيل في الوظيفة التي تم إرجاعها، لأن قائمتنا المرتبطة بالتأجيل يمكن أن تحتوي على العديد من التأجيلات من العديد من الوظائف المختلفة.
func B() { defer fmt.Println(1) defer fmt.Println(2) A() } func A() { defer fmt.Println(3) defer fmt.Println(4) }
لذلك، يتم تنفيذ الوظائف المؤجلة فقط في الوظيفة الحالية (أو إطار المكدس الحالي).
ولكن هناك حالة نموذجية واحدة حيث يتم تتبع وتنفيذ جميع الوظائف المؤجلة في goroutine الحالي، وذلك عندما يحدث الذعر.
إلى جانب أخطاء وقت الترجمة، لدينا مجموعة من أخطاء وقت التشغيل: القسمة على صفر (عدد صحيح فقط)، خارج الحدود، إلغاء الإشارة إلى مؤشر صفر، وما إلى ذلك. تتسبب هذه الأخطاء في ذعر التطبيق.
الذعر هو وسيلة لإيقاف تنفيذ goroutine الحالي، وتفكيك المكدس، وتنفيذ الوظائف المؤجلة في goroutine الحالي، مما يتسبب في تعطل تطبيقنا.
للتعامل مع الأخطاء غير المتوقعة ومنع التطبيق من التعطل، يمكنك استخدام وظيفة الاسترداد ضمن وظيفة مؤجلة لاستعادة السيطرة على goroutine المذعور.
func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() panic("This is a panic") } // Output: // Recovered: This is a panic
عادةً، يضع الأشخاص خطأً في حالة الذعر ويكتشفون ذلك باستخدام الاسترداد (..)، ولكن يمكن أن يكون أي شيء: سلسلة نصية، أو int، وما إلى ذلك.
في المثال أعلاه، داخل الوظيفة المؤجلة هو المكان الوحيد الذي يمكنك استخدام الاسترداد فيه. اسمحوا لي أن أشرح هذا أكثر قليلا.
هناك بعض الأخطاء التي يمكننا ذكرها هنا. لقد رأيت على الأقل ثلاثة مقتطفات كهذه في كود حقيقي.
الأول هو استخدام الاسترداد مباشرة كوظيفة مؤجلة:
func main() { defer recover() panic("This is a panic") }
لا يزال الكود أعلاه يثير الذعر، وهذا حسب تصميم وقت تشغيل Go.
تهدف وظيفة الاسترداد إلى إثارة الذعر، ولكن يجب استدعاؤها ضمن وظيفة مؤجلة لتعمل بشكل صحيح.
خلف الكواليس، دعوتنا للاسترداد هي في الواقع runtime.gorecover، وهي تتحقق من أن استدعاء الاسترداد يحدث في السياق الصحيح، وتحديدًا من الوظيفة المؤجلة الصحيحة التي كانت نشطة عند حدوث الذعر.
"هل هذا يعني أنه لا يمكننا استخدام الاسترداد في وظيفة داخل وظيفة مؤجلة، مثل هذا؟"
func myRecover() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } } func main() { defer func() { myRecover() // ... }() panic("This is a panic") }
بالضبط، لن يعمل الكود أعلاه كما تتوقع. وذلك لأن الاسترداد لا يتم استدعاؤه مباشرة من دالة مؤجلة ولكن من دالة متداخلة.
الآن، خطأ آخر هو محاولة التقاط حالة من الذعر من نظام مختلف:
func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() go panic("This is a panic") time.Sleep(1 * time.Second) // Wait for the goroutine to finish }
من المنطقي، أليس كذلك؟ نحن نعلم بالفعل أن سلاسل التأجيل تنتمي إلى نظام محدد. سيكون الأمر صعبًا إذا تمكن أحد goroutine من التدخل في جهاز آخر للتعامل مع حالة الذعر نظرًا لأن كل goroutine لديه مجموعته الخاصة.
لسوء الحظ، فإن السبيل الوحيد للخروج في هذه الحالة هو تعطل التطبيق إذا لم نتعامل مع الذعر في هذا goroutine.
لقد واجهت هذه المشكلة من قبل، حيث تم دفع البيانات القديمة إلى نظام التحليلات، وكان من الصعب معرفة السبب.
إليك ما أعنيه:
func pushAnalytic(a int) { fmt.Println(a) } func main() { a := 10 defer pushAnalytic(a) a = 20 }
ما رأيك في النتيجة؟ إنها 10 وليست 20.
وذلك لأنه عند استخدام عبارة التأجيل، فإنه يلتقط القيم مباشرة. وهذا ما يسمى "الالتقاط حسب القيمة". لذلك، يتم تعيين قيمة a التي يتم إرسالها إلى PushAnalytic على 10 عند جدولة التأجيل، على الرغم من حدوث تغييرات لاحقًا.
هناك طريقتان لإصلاح ذلك.
...
المنشور الكامل متاح هنا: Golang Defer: من Basic To Trap.
تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.
Copyright© 2022 湘ICP备2022001581号-3