defer
الكلمة المفتاحية defer ذات تكرار عالٍ جدًا في التطوير اليومي لـ Go، وتُنفَّذ بطريقة الداخل أولًا الخارج أخيرًا للدوال المرتبطة بـ defer، وغالبًا ما نستخدم هذه الآلية لإجراء عمليات تحرير الموارد، مثل إغلاق الملفات وغيرها.
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...كونها كلمة مفتاحية ذات تكرار عالٍ، يجعل من الضروري فهم بنيتها الأساسية.
البنية
الكلمة المفتاحية defer تقابلها البنية runtime._defer، وبنيتها ليست معقدة:
type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn func() // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer // next defer on G; can point to either heap or stack!
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
framepc uintptr
}الحقل fn هو الدالة المقابلة للكلمة المفتاحية defer، و link يشير لـ defer التالي في السلسلة، و sp و pc يسجلان معلومات الدالة المستدعية، ويُستخدمان للحكم على الدالة التي ينتمي إليها defer. يوجد defer في وقت التشغيل كقائمة مرتبطة، ورأس القائمة موجود على الكوروتين G، لذا فإن defer في الواقع مرتبط مباشرة بالكوروتين.
type g struct {
...
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
...
}عندما ينفذ الكوروتين دالة، يُضيف defer الخاص بالدالة من رأس القائمة المرتبطة بالترتيب:
defer fn1()
defer fn2()
defer fn3()الكود أعلاه يوافق هذه الصورة:

بخلاف الكوروتين، P أيضًا له علاقة مع defer، ففي بنية P يوجد حقل deferpool، كما يلي:
type p struct {
...
deferpool []*_defer // pool of available defer structs (see panic.go)
deferpoolbuf [32]*_defer
...
}يحتوي deferpool على هياكل defer مُخصصة مسبقًا، لتُستخدم لتخصيص هياكل defer جديدة للكوروتين G المرتبط بـ P، مما يقلل التكلفة.
التخصيص
من الناحية النحوية، استخدام الكلمة المفتاحية defer سيُحوِّله المترجم لاستدعاء الدالة runtime.deferproc. مثلًا، كود Go يُكتب هكذا:
defer fn1(x, y)وبعد الترجمة يكون الكود الفعلي هكذا:
deferproc(func(){
fn1(x, y)
})لذا فإن الدالة المُمررة لـ defer فعليًا ليس لها معاملات ولا قيمة إرجاع، وكود الدالة deferproc كالتالي:
func deferproc(fn func()) {
gp := getg()
d := newdefer()
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = getcallerpc()
d.sp = getcallersp()
return0()
}هذه الدالة مسؤولة عن إنشاء بنية defer وإضافتها لرأس القائمة المرتبطة للكوروتين G، والدالة runtime.newdefer ستحاول الحصول على بنية defer مُخصصة مسبقًا من deferpool في P.
if len(pp.deferpool) == 0 && sched.deferpool != nil {
lock(&sched.deferlock)
for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
d := sched.deferpool
sched.deferpool = d.link
d.link = nil
pp.deferpool = append(pp.deferpool, d)
}
unlock(&sched.deferlock)
}أولًا سيأخذ نصف هياكل defer من sched.deferpool العام ويملأ بها deferpool المحلي، ثم سيحاول الحصول على من deferpool في P:
if n := len(pp.deferpool); n > 0 {
d = pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
}
if d == nil {
// Allocate new defer.
d = new(_defer)
}
d.heap = trueأخيرًا إذا لم يجد سيستخدم التخصيص اليدوي. ويمكن رؤية هذا الكود:
d.heap = trueهذا يعني أن defer مُخصَّص على الكومة، وبالمقابل عندما يكون false، سيُخصَّص على المكدس، والذاكرة المُخصصة على المكدس تُسترد تلقائيًا عند الإرجاع، وكفاءة إدارتها للذاكرة أعلى من الكومة، والعامل الذي يحدد ما إذا كان سيُخصَّص على المكدس هو عدد طبقات الحلقة، ويمكن تتبع هذا المنطق للدالة escape.goDeferStmt في cmd/compile/ssagen:
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
...
if n.Op() == ir.ODEFER && e.loopDepth == 1 {
k = e.later(e.discardHole())
n.SetEsc(ir.EscNever)
}
...
}e.loopDepth يمثل عدد طبقات الحلقة للعبارة الحالية، إذا لم تكن عبارة defer الحالية داخل حلقة، فسيتم تخصيصها على المكدس.
case ir.ODEFER:
n := n.(*ir.GoDeferStmt)
if s.hasOpenDefers {
s.openDeferRecord(n.Call.(*ir.CallExpr))
} else {
d := callDefer
if n.Esc() == ir.EscNever {
d = callDeferStack
}
s.callResult(n.Call.(*ir.CallExpr), d)
}إذا كان التخصيص على المكدس، فستُنشأ بنية defer مباشرة على المكدس، وأخيرًا ستُكمِل الدالة runtime.deferprocStack إنشاء بنية defer.
if k == callDeferStack {
// Make a defer struct d on the stack.
if stksize != 0 {
s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
}
t := deferstruct()
...
// Call runtime.deferprocStack with pointer to _defer record.
ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
callArgs = append(callArgs, addr, s.mem())
call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
call.AddArgs(callArgs...)
call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer argتوقيع الدالة deferprocStack:
func deferprocStack(d *_defer)منطق الإنشاء المحدد ليس مختلفًا كثيرًا عن deferproc، والفرق الرئيسي هو أن مصدر بنية defer عند التخصيص على المكدس هو بنية مُنشأة مباشرة، بينما مصدر defer المُخصص على الكومة هو الدالة new.
التنفيذ
عندما تكون الدالة على وشك الإرجاع أو عند حدوث panic، سيُدخَل في الدالة runtime.deferreturn، وهي مسؤولة عن إخراج defer من القائمة المرتبطة للكوروتين وتنفيذه.
func deferreturn() {
gp := getg()
for {
d := gp._defer
sp := getcallersp()
if d.sp != sp {
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
fn()
}
}أولًا ستُحصل على إطار المكدس للدالة الحالية عبر getcallersp() وتقارنه مع sp في بنية defer للحكم على ما إذا كان defer ينتمي للدالة الحالية، ثم تُخرج بنية defer من رأس القائمة المرتبطة، وتستخدم gp._defer = d.link للإشارة لـ defer التالي، ثم عبر الدالة runtime.freedefer تُطلق بنية defer إلى التجمع، وأخيرًا تستدعي fn للتنفيذ، وتستمر هكذا حتى تنتهي من تنفيذ جميع defer الخاصة بالدالة الحالية.
الترميز المفتوح
استخدام defer ليس بلا تكلفة، رغم أنه يوفر لنا راحة نحوية، لكنه في النهاية ليس استدعاء دالة مباشر، بل يمر بسلسلة من العمليات، لذا يسبب استهلاكًا في الأداء، لذلك صمم فريق Go لاحقًا طريقة تحسين تسمى الترميز المفتوح، وهي طريقة لتحسين defer، واسمها الأصلي بالإنجليزية open-coded، وتُرجمت محليًا للترميز المفتوح، وكلمة open هنا تعني التوسيع، أي توسيع كود دالة defer في كود الدالة الحالية، مثلما يحدث مع الدوال المضمنة. طريقة التحسين هذه لها الشروط التالية:
- عدد
deferفي الدالة لا يتجاوز 8 - حاصل ضرب عدد
deferوعددreturnلا يتجاوز 15 deferلا يظهر في حلقة- لم يُعطَّل تحسين المترجم
- لم يُستدعَ
os.Exit()يدويًا - لا حاجة لنسخ المعاملات من الكومة
يمكن تتبع منطق الحكم هذا في الدالة cmd/compile/ssagen.buildssa:
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
s.hasOpenDefers = false
}
if s.hasOpenDefers {
for _, f := range s.curfn.Type().Results().FieldSlice() {
if !f.Nname.(*ir.Name).OnStack() {
s.hasOpenDefers = false
break
}
}
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
s.hasOpenDefers = false
}ثم سينشئ Go في الدالة الحالية متغير صحيح 8-بت deferBits ليكون bitmap لتمييز defer، كل بت يمثل واحدًا، والعدد الصحيح 8-بت uint8 يمثل 8 كحد أقصى، إذا كان البت المقابل يساوي 1، فإن defer المُحسَّن بالترميز المفتوح سيُنفَّذ عندما توشك الدالة على الإرجاع.
