گزاره defer در زبان برنامه‌نویسی Go — راهنمای کاربردی

۲۰۲ بازدید
آخرین به‌روزرسانی: ۱۲ مهر ۱۴۰۲
زمان مطالعه: ۴ دقیقه
گزاره defer در زبان برنامه‌نویسی Go — راهنمای کاربردی

گزاره defer در زبان برنامه‌نویسی Go یک روش آسان برای اجرای یک قطعه کد پیش از بازگشت تابع است. طبق مستندات Go تابع‌های defer شده به جای اجرای بی‌درنگ پیش از بازگشت تابع پیرامونی، به طور عکس خودشان به تأخیر می‌افتند. در ادامه مثالی از پیاده‌سازی LIFO را می‌بینید:

997696
1func main() {
2   defer func() {
3      println(`defer 1`)
4   }()
5   defer func() {
6      println(`defer 2`)
7   }()
8}
9defer 2 <- Last in, first to go out
10defer 1

در ادامه به توضیح سازوکار داخلی این گزاره می‌پردازیم و سپس یک حالت پیچیده‌تر را مطرح می‌کنیم.

پیاده‌سازی داخلی

محیط زمان اجرای Go اقدام به پیاده‌سازی LIFO با استفاده از یک لیست پیوندی می‌کند. در واقع یک struct تأخیر دار لینکی به struct بعدی که باید اجرا شود دارد:

1type _defer struct {
2   siz     int32
3   started bool
4   sp      uintptr
5   pc      uintptr
6   fn      *funcval
7   _panic  *_panic
8   link    *_defer // next deferred function to be executed
9}

زمانی که یک متد defer جدید ایجاد می‌شود، به Goroutine جاری وصل می‌شود و قبلی به متد فعلی لینک می‌شود چون تابع بعدی باید اجرا شود:

1func newdefer(siz int32) *_defer {
2   var d *_defer
3   gp := getg() // get the current goroutine
4   [...]
5   // deferred list is now attached to the new _defer struct
6   d.link = gp._defer 
7   gp._defer = d // the new struct is now the first to be called
8   return d
9}

فراخوانی‌های متوالی اینک بدون پشته هستند و تابع‌های defer شده از بالا به صورت زیر هستند:

1func deferreturn(arg0 uintptr) {
2   gp := getg() // get the current goroutine
3   d:= gp._defer // copy the deferred function to a variable
4   if d == nil { // if there is not deferred func, just return
5      return
6   }
7   [...]
8   fn := d.fn // get the function to call
9   d.fn = nil // reset the function
10   gp._defer = d.link // attach the next one to the goroutine
11   freedefer(d) // freeing the _defer struc
12   jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // call the func
13}

چنان که می‌بینیم ما تابع‌های defer شده را در حلقه قرار نمی‌دهیم، بلکه یک به یک از پشته خارج می‌شوند. این رفتار از سوی کد ASM تولیدشده تأیید می‌شود:

1// first deferred func
20x001d 00029 (main.go:6)   MOVL   $0, (SP)
30x0024 00036 (main.go:6)   PCDATA $2, $1
40x0024 00036 (main.go:6)   LEAQ   "".main.func1·f(SB), AX
50x002b 00043 (main.go:6)   PCDATA $2, $0
60x002b 00043 (main.go:6)   MOVQ   AX, 8(SP)
70x0030 00048 (main.go:6)   CALL   runtime.deferproc(SB)
80x0035 00053 (main.go:6)   TESTL  AX, AX
90x0037 00055 (main.go:6)   JNE    117
10// second deferred func
110x0039 00057 (main.go:10)  MOVL   $0, (SP)
120x0040 00064 (main.go:10)  PCDATA $2, $1
130x0040 00064 (main.go:10)  LEAQ   "".main.func2·f(SB), AX
140x0047 00071 (main.go:10)  PCDATA $2, $0
150x0047 00071 (main.go:10)  MOVQ   AX, 8(SP)
160x004c 00076 (main.go:10)  CALL   runtime.deferproc(SB)
170x0051 00081 (main.go:10)  TESTL  AX, AX
180x0053 00083 (main.go:10)  JNE    101
19// end of main func
200x0055 00085 (main.go:18)  XCHGL  AX, AX
210x0056 00086 (main.go:18)  CALL   runtime.deferreturn(SB)
220x005b 00091 (main.go:18)  MOVQ   16(SP), BP
230x0060 00096 (main.go:18)  ADDQ   $24, SP
240x0064 00100 (main.go:18)  RET
250x0065 00101 (main.go:10)  XCHGL  AX, AX
260x0066 00102 (main.go:10)  CALL   runtime.deferreturn(SB)
270x006b 00107 (main.go:10)  MOVQ   16(SP), BP
280x0070 00112 (main.go:10)  ADDQ   $24, SP
290x0074 00116 (main.go:10)  RET

متد deferproc دو بار فراخوانی می‌شود و به صورت درونی متد newdefer که قبلاً برای ثبت تابعمان به صوت متدهای defer شده دیدیم، فراخوانی می‌کند. در نهایت در پایان تابع، متد defer شده به لطف تابع deferreturn یک به یک فراخوانی می‌شود.

کتابخانه Go به ما نشان داد که struct به نام defer_‎ به خصوصیت panic *_panic_‎ نیز لینک شده است. در ادامه به بررسی فایده آن در طی یک مثال دیگر می‌پردازیم.

Defer و مقدار بازگشتی

تنها راه برای یک تابع defer شده جهت دسترسی به نتیجه بازگشتی استفاده از پارامتر نتیجه با نام است که در مشخصات چنین توصیف شده است:

اگر تابع defer شده یک function literal باشد و تابع پیرامونی دارای پارامترهای نتیجه با نام باشد که در دامنه literal قرار دارند، تابع defer شده می‌تواند به پارامترهای نتیجه پیش از بازگشت دسترسی داشته و آن‌ها را تغییر دهد.

به مثال زیر توجه کنید:

1func main() {
2   fmt.Printf("with named param, x: %d\n", namedParam())
3   fmt.Printf("without named param, x: %d\n", notNamedParam())
4}
5func namedParam() (x int) {
6   x = 1
7   defer func() { x = 2 }()
8   return x
9}
10
11func notNamedParam() (int) {
12   x := 1
13   defer func() { x = 2 }()
14   return x
15}
16with named param, x: 2
17without named param, x: 1

زمانی که این رفتار درک شود می‌توانیم آن را با تابع Recover ترکیب کنیم. در واقع Recover یک تابع درونی است که کنترل یک goroutine دارای panic را به دست می‌گیرد. Recover تنها درون تابع‌های defer شده مفید است.

چنان که دیدیم struct به نام defer_‎ به خصوصیت panic_‎ لینک شده است و این کار در طی یک فراخوانی panic روی می‌دهد:

1func gopanic(e interface{}) {
2   [...]
3   var p _panic
4   [...]
5   d := gp._defer // current attached defer on the goroutine
6   [...]
7   d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
8   [...]
9}

در واقع متد gopanic در حالت panic پیش از فراخوانی تابع‌های defer شده فراخوانی می‌شود:

10x0067 00103 (main.go:21)   CALL   runtime.gopanic(SB)
20x006c 00108 (main.go:21)  UNDEF
30x006e 00110 (main.go:16)  XCHGL  AX, AX
40x006f 00111 (main.go:16)  CALL   runtime.deferreturn(SB)

در ادامه یک مثال از تابع recover را می‌بینید که از مزیت پارامترهای نتیجه با نام استفاده می‌کند:

1func main() {
2   fmt.Printf("error from err1: %v\n", err1())
3   fmt.Printf("error from err2: %v\n", err2())
4}
5
6func err1() error {
7   var err error
8
9   defer func() {
10      if r := recover(); r != nil {
11         err = errors.New("recovered")
12      }
13   }()
14   panic(`foo`)
15
16   return err
17}
18
19func err2() (err error) {
20   defer func() {
21      if r := recover(); r != nil {
22         err = errors.New("recovered")
23      }
24   }()
25   panic(`foo`)
26
27   return err
28}
29error from err1: <nil>
30error from err2: recovered

الحاق این دو به ما کمک می‌کند که به طرز مناسبی با تابع Recover کار کنیم و می‌توانیم خطاهایمان را به فراخوانی کننده بازگشت دهیم. برای نتیجه‌گیری از این مقاله در مورد تابع‌های defer شده نگاهی به بهبودهایی که برای ما فراهم می‌سازند می‌اندازیم.

بهبود عملکرد

آخرین نسخه‌ای که کاربرد defer را بهبود بخشیده است، نسخه 1.8 Go است. ما با اجرای بنچمارک در کتابخانه Go (بین نسخه‌های 1.78 و 1.8) می‌توانیم این بهبود را مشاهده کنیم:

1name         old time/op  new time/op  delta
2Defer-4      99.0ns ± 9%  52.4ns ± 5%  -47.04%  (p=0.000 n=9+10)
3Defer10-4    90.6ns ±13%  45.0ns ± 3%  -50.37%  (p=0.000 n=10+10)

این بهبود به لطف بهینه‌سازی روش تخصیص و جلوگیری از رشد پشته حاصل شده است. همچنین یک بهینه‌سازی برای گزاره defer بدون هیچ آرگومان وجود دارد که از یک کپی حافظه جلوگیری می‌کند. در ادامه بنچمارک یک تابع defer شده با/بدون آرگومان مقایسه شده است:

1name     old time/op  new time/op  delta
2Defer-4  51.3ns ± 3%  45.8ns ± 1%  -10.72%  (p=0.000 n=10+10)

این کد هم اینک به لطف بهینه‌سازی دوم 10 درصد سریع‌تر شده است.

اگر این مطلب برای شما مفید بوده است، آموزش‌ها زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
blanchon.vincent
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *