"إذا أراد العامل أن يؤدي عمله بشكل جيد، فعليه أولاً أن يشحذ أدواته." - كونفوشيوس، "مختارات كونفوشيوس. لو لينجونج"
الصفحة الأمامية > برمجة > Golang Defer: تأجيل مخصص للكومة، مخصص للمكدس، تأجيل مشفر مفتوح

Golang Defer: تأجيل مخصص للكومة، مخصص للمكدس، تأجيل مشفر مفتوح

تم النشر بتاريخ 2024-08-19
تصفح:272

هذا مقتطف من التدوينة؛ المنشور الكامل متاح هنا: Golang Defer: من Basic To Trap.

ربما يكون بيان التأجيل أحد الأشياء الأولى التي نجدها مثيرة للاهتمام عندما نبدأ في تعلم Go، أليس كذلك؟

ولكن هناك الكثير مما يزعج العديد من الأشخاص، وهناك العديد من الجوانب الرائعة التي غالبًا ما لا نتطرق إليها عند استخدامه.

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

مخصص للكومة، ومخصص للمكدس، وتأجيل مفتوح الترميز

على سبيل المثال، تحتوي عبارة التأجيل فعليًا على 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 الحالي، مثل هذا:

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

سلسلة تأجيل Goroutine

وعندما تعود الدالة، فإنها تمر عبر القائمة المرتبطة وتنفذ كل واحدة منها بالترتيب الموضح في الصورة أعلاه.

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

func B() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  A()
}

func A() {
  defer fmt.Println(3)
  defer fmt.Println(4)
}

لذلك، يتم تنفيذ الوظائف المؤجلة فقط في الوظيفة الحالية (أو إطار المكدس الحالي).

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

سلسلة تأجيل Goroutine

ولكن هناك حالة نموذجية واحدة حيث يتم تتبع وتنفيذ جميع الوظائف المؤجلة في 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.

بيان الافراج تم إعادة إنتاج هذه المقالة على: https://dev.to/func25/golang-defer-heap-allocated-stack-allocated-open-coded-defer-1h9o?1 إذا كان هناك أي انتهاك، يرجى الاتصال بـ [email protected] لحذفه
أحدث البرنامج التعليمي أكثر>

تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.

Copyright© 2022 湘ICP备2022001581号-3