قُدّمت استدعاءات الدوال المؤجَّلة سابقاً. ونظراً لمحدودية المعرفة بـGo في ذلك الوقت، لم تتناول تلك المقالة بعض التفاصيل وحالات الاستخدام الإضافية لاستدعاءات الدوال المؤجَّلة. وستُتناول هذه التفاصيل وحالات الاستخدام في ما تبقّى من هذه المقالة.
لا يمكن تأجيل استدعاءات كثير من الدوال المدمجة ذات نتائج الإرجاع
في Go، يمكن إهمال جميع قيم نتائج استدعاء الدوال المخصّصة (أي حذفها). لكن بالنسبة إلى الدوال المدمجة ذات قوائم نتائج الإرجاع غير الفارغة، يجب ألا تكون قيم نتائج استدعاءاتها مهملة، باستثناء الاستدعاءات إلى الدالتين المدمجتين copy وrecover. ومن جهة أخرى، تعلّمنا أنه يجب إهمال قيم نتائج استدعاء الدالة المؤجَّل، لذا لا يمكن تأجيل استدعاءات كثير من الدوال المدمجة.
لحسن الحظ، نادراً ما نحتاج عملياً إلى تأجيل استدعاءات الدوال المدمجة (ذات قوائم نتائج الإرجاع غير الفارغة). وعلى حدّ علمي، قد يلزم أحياناً تأجيل الاستدعاءات إلى الدالة المدمجة append فقط. وفي هذه الحالة، يمكننا تأجيل استدعاء دالة مجهولة تغلّف استدعاء append.
package main
import "fmt"
func main() {
s := []string{"a", "b", "c", "d"}
defer fmt.Println(s) // [a x y d]
// defer append(s[:1], "x", "y") // خطأ
defer func() {
_ = append(s[:1], "x", "y")
}()
}
لحظة تقييم قيم الدوال المؤجَّلة
تُقيَّم الدالة المستدعاة (قيمتها) في استدعاء الدالة المؤجَّل عند دفع الاستدعاء إلى مكدس استدعاءات defer الخاص بالـgoroutine الحالية. على سبيل المثال، سيطبع البرنامج التالي false.
package main
import "fmt"
func main() {
var f = func () {
fmt.Println(false)
}
defer f()
f = func () {
fmt.Println(true)
}
}
قد تكون الدالة المستدعاة في استدعاء الدالة المؤجَّل قيمة دالة nil. وفي هذه الحالة، سيحدث panic عند استدعاء الدالة nil، بدلاً من حدوثه عند دفع الاستدعاء إلى مكدس استدعاءات defer الخاص بالـgoroutine الحالية. مثال:
package main
import "fmt"
func main() {
defer fmt.Println("reachable 1")
var f func() // قيمة f هي nil افتراضياً
defer f() // panic هنا
// الأسطر التالية يمكن الوصول إليها أيضاً.
fmt.Println("reachable 2")
f = func() {} // لا جدوى منه لتجنّب حدوث panic
}
لحظة تقييم معطيات المستقبِل في استدعاءات الطرق المؤجَّلة
كما شُرح سابقاً، تُقيَّم معطيات استدعاء الدالة المؤجَّل أيضاً عند دفع الاستدعاء المؤجَّل إلى مكدس استدعاءات defer الخاص بالـgoroutine الحالية.
معطيات مستقبِل الطرق ليست استثناءً من ذلك أيضاً. على سبيل المثال، يطبع البرنامج التالي 1342.
package main
type T int
func (t T) M(n int) T {
print(n)
return t
}
func main() {
var t T
// "t.M(1)" هو معطى المستقبِل لاستدعاء
// الطريقة ".M(2)"، لذا يُقيَّم عند دفع
// الاستدعاء ".M(2)" إلى مكدس استدعاءات defer.
defer t.M(1).M(2)
t.M(3).M(4)
}
الاستدعاءات المؤجَّلة تجعل الشيفرة أنظف وأقل عرضة للأخطاء
مثال:
import "os"
func withoutDefers(filepath string, head, body []byte) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
_, err = f.Seek(16, 0)
if err != nil {
f.Close()
return err
}
_, err = f.Write(head)
if err != nil {
f.Close()
return err
}
_, err = f.Write(body)
if err != nil {
f.Close()
return err
}
err = f.Sync()
f.Close()
return err
}
func withDefers(filepath string, head, body []byte) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
defer f.Close()
_, err = f.Seek(16, 0)
if err != nil {
return err
}
_, err = f.Write(head)
if err != nil {
return err
}
_, err = f.Write(body)
if err != nil {
return err
}
return f.Sync()
}
أيّهما يبدو أنظف؟ من الواضح أن الذي يستخدم الاستدعاءات المؤجَّلة، وإن كان أنظف قليلاً فقط. وهو أقل عرضة للأخطاء أيضاً، لأن في الدالة التي لا تستخدم الاستدعاءات المؤجَّلة عدداً كبيراً جداً من استدعاءات f.Close()، ما يزيد احتمال نسيان أحدها.
في ما يلي مثال آخر يُظهر أن الاستدعاءات المؤجَّلة يمكن أن تجعل الشيفرة أقل عرضة للأخطاء. إذا حدث panic في استدعاءات doSomething في المثال التالي، فستخرج الدالة f2 دون تحرير قيمة Mutex. لذا فإن الدالة f1 أقل عرضة للأخطاء.
var m sync.Mutex
func f1() {
m.Lock()
defer m.Unlock()
doSomething()
}
func f2() {
m.Lock()
doSomething()
m.Unlock()
}
خسائر الأداء الناتجة عن تأجيل استدعاءات الدوال
ليس استخدام استدعاءات الدوال المؤجَّلة جيداً دائماً. فبالنسبة إلى مصرِّف Go الرسمي، قبل الإصدار 1.13، كانت استدعاءات الدوال المؤجَّلة تسبّب بعض خسائر الأداء في وقت التشغيل. ومنذ سلسلة أدوات Go 1.13، حُسّنت كثير من حالات استخدام defer الشائعة تحسيناً كبيراً، بحيث لا نحتاج عموماً إلى الاهتمام بمشكلة خسائر الأداء الناتجة عن الاستدعاءات المؤجَّلة. الشكر لـDan Scales على التحسينات الرائعة التي أجراها.
نوع من تسرّب الموارد بسبب تأجيل استدعاءات الدوال
قد يستهلك مكدس استدعاءات defer الضخم جداً ذاكرة كبيرة أيضاً، وقد لا تُحرَّر بعض الموارد في الوقت المناسب إذا تأخّرت بعض الاستدعاءات كثيراً.
على سبيل المثال، إذا لزمت معالجة ملفات كثيرة في استدعاء للدالة التالية، فلن يُحرَّر عدد كبير من معالِجات الملفات قبل خروج الدالة.
func writeManyFiles(files []File) error {
for _, file := range files {
f, err := os.Open(file.path)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
err = f.Sync()
if err != nil {
return err
}
}
return nil
}
في مثل هذه الحالات، يمكننا استخدام دالة مجهولة لتغليف الاستدعاءات المؤجَّلة بحيث تُنفَّذ استدعاءات الدوال المؤجَّلة أبكر. على سبيل المثال، يمكن إعادة كتابة الدالة أعلاه وتحسينها كما يلي
func writeManyFiles(files []File) error {
for _, file := range files {
if err := func() error {
f, err := os.Open(file.path)
if err != nil {
return err
}
// ستُستدعى الطريقة Close عند
// نهاية خطوة الحلقة الحالية.
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
return f.Sync()
}(); err != nil {
return err
}
}
return nil
}