ماذا تفعل defer
تدفع defer استدعاء دالة إلى قائمة. وعند عودة الدالة المحيطة تُنفَّذ القائمة بترتيب معكوس.
المخرجات:
start
end
deferred 3
deferred 2
deferred 1
الترتيب هو الأخير دخولًا الأول خروجًا (LIFO)، مثل المكدّس. وهذا يطابق طريقة تداخل الموارد: إذا فتحت A ثم B، فأنت تريد عادة إغلاق B قبل A.
التنظيف بجوار الحصول على المورد
الاستخدام الأساسي لـ defer هو كتابة التنظيف في السطر التالي مباشرة للحصول على المورد، فلا يستطيع أي مسار عودة أن ينساه.
لاحظ الترتيب: افحص الخطأ أولًا، ثم أجّل. إذا فشلت os.Open يكون f مساويًا لـ nil، وتأجيل f.Close() قبل الفحص سيستدعي Close على *os.File قيمته nil (وهذا يعيد خطأ لن تراه أبدًا، ويضلّل القرّاء).
الشكل نفسه يعمل مع الأقفال:
mu.Lock()
defer mu.Unlock()
إذا سبّبت الشيفرة بينهما panic، يبقى الـ mutex مفكوكًا.
الوسائط تُقيَّم فورًا
تُقيَّم الدالة المؤجّلة ووسائطها عند تنفيذ عبارة defer. الاستدعاء وحده ينتظر.
المخرجات:
x is now 2
deferred closure reads: 2
deferred with argument: 1
التقطت fmt.Println("...", x) القيمة 1 عند سطر defer. أما الإغلاق فلا وسائط له؛ يقرأ x عند تنفيذه أخيرًا. اختر الشكل الذي يطابق ما تريد تسجيله.
ينطبق هذا أيضًا على مستقبِلات التوابع. تقيّم defer t.Stop() المتغيّر t فورًا، فإعادة إسناد t لاحقًا لا تغيّر القيمة التي ستُوقَف.
حيلة توقيت شائعة تستخدم هذه القاعدة عمدًا:
func handle() {
defer trace("handle")() // trace runs now, the returned func runs at exit
// ...
}
تُستدعى trace("handle") فورًا (فيمكنها طباعة "enter" وتسجيل وقت البدء)، والدالة التي تعيدها هي ما يُؤجَّل.
defer داخل حلقة
تُنفَّذ الاستدعاءات المؤجّلة عند عودة الدالة، لا في نهاية كل تكرار. في حلقة على ملفات كثيرة، يبقي هذا كل ملف مفتوحًا حتى تنتهي الدالة.
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // all files stay open until the function returns
process(f)
}
مع آلاف المسارات تنفد واصفات الملفات. انقل جسم الحلقة إلى دالة خاصة به لتُنفَّذ كل defer في كل تكرار:
كل استدعاء لـ processFile يغلق ملفه قبل أن يُفتح التالي. والدالة الحرفية المستدعاة في مكانها (func() { ... }()) تعمل بالطريقة نفسها عندما تبدو الدالة المساعدة المسمّاة مبالغة.
تعديل القيم المعادة
يُنفَّذ الإغلاق المؤجّل بعد أن تسند عبارة return النتائج، ويمكنه تعديل النتائج المسمّاة قبل أن يراها المستدعي.
يطبع هذا 10 وsave failed: disk full. مع نتيجة غير مسمّاة يمكن للدالة المؤجّلة أن تُنفَّذ، لكنها لا تملك طريقة لتغيير ما يُعاد.
التقاط خطأ Close
تتجاهل defer f.Close() الخطأ الصادر عن Close. للملفات التي تقرؤها فقط لا بأس بذلك. أما للملفات التي كتبت فيها فقد تبلغ Close عن فشل التفريغ الأخير، فيصبح الخطأ مهمًا. النتيجة المسمّاة تتيح لك الاحتفاظ به:
func writeReport(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
_, err = f.Write(data)
return err
}
errors.Join(err, f.Close()) بديل أقصر عندما تريد الإبلاغ عن الخطأين.
defer وpanic وrecover
تُنفَّذ الاستدعاءات المؤجّلة أثناء فكّ الـ panic للمكدّس. وهذا المكان الوحيد الذي تفعل فيه recover شيئًا، وهو الطريقة التي يمنع بها الخادم طلبًا سيئًا واحدًا من قتل العملية. التفاصيل في صفحة panic وrecover.
لا تُنفَّذ الاستدعاءات المؤجّلة عندما يخرج البرنامج عبر os.Exit أو log.Fatal. إذا أجّلت main تنظيفًا ثم استدعت os.Exit(1)، يُتخطّى التنظيف.
الكلفة
منذ Go 1.14 يضمّن المترجم معظم عبارات defer مباشرة في الشيفرة (open-coded) فتكلّف بضع نانوثوانٍ. استخدام defer لكل تحرير mutex وإغلاق ملف هو الأسلوب المعتاد. الاستثناء هو defer داخل الحلقات: لا يمكن تضمينها مباشرة فترجع إلى مسار أبطأ، وهذا سبب إضافي لنقل أجسام الحلقات إلى دوال.
أخطاء شائعة
- التأجيل قبل فحص الخطأ. افحص
errالصادر عنOpenأولًا، ثمdefer Close. - توقّع تنظيف في كل تكرار داخل حلقة. تُنفَّذ defer عند خروج الدالة.
- توقّع أن يرى الوسيط المؤجّل التغييرات اللاحقة. الوسائط تُثبَّت عند سطر
defer. استخدم إغلاقًا للقراءة وقت الخروج. - الاعتماد على defer مع
os.Exit. لا تُنفَّذ أبدًا.
الأسئلة الشائعة
ماذا تفعل defer في Go؟
تجدول defer f() تنفيذ f() عند عودة الدالة المحيطة، سواء عادت بشكل طبيعي، أو عبر return مبكّرة، أو بسبب panic. تُستخدم لوضع التنظيف (إغلاق ملف، فكّ قفل mutex) بجوار الشيفرة التي حصلت على المورد مباشرة.
بأي ترتيب تُنفَّذ الاستدعاءات المؤجّلة في Go؟
الأخير دخولًا هو الأول خروجًا. آخر استدعاء مؤجّل يُنفَّذ أولًا. defer fmt.Println(1); defer fmt.Println(2) تطبع 2 ثم 1.
متى تُقيَّم وسائط الدالة المؤجّلة؟
فورًا، عند تنفيذ عبارة defer، لا عند تنفيذ الاستدعاء. x := 1; defer fmt.Println(x); x = 2 تطبع 1. لقراءة القيمة وقت الخروج، أجّل إغلاقًا: defer func() { fmt.Println(x) }().
هل تُنفَّذ defer عند panic أو os.Exit؟
تُنفَّذ الاستدعاءات المؤجّلة أثناء فكّ الـ panic للمكدّس، ولهذا تعمل recover داخلها. ولا تُنفَّذ عندما يستدعي البرنامج os.Exit (أو log.Fatal التي تستدعيها)، ولا تُنفَّذ في الـ goroutines الأخرى عندما تعود main.