گزاره defer در زبان برنامهنویسی Go — راهنمای کاربردی
گزاره defer در زبان برنامهنویسی Go یک روش آسان برای اجرای یک قطعه کد پیش از بازگشت تابع است. طبق مستندات Go تابعهای defer شده به جای اجرای بیدرنگ پیش از بازگشت تابع پیرامونی، به طور عکس خودشان به تأخیر میافتند. در ادامه مثالی از پیادهسازی LIFO را میبینید:
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 درصد سریعتر شده است.
اگر این مطلب برای شما مفید بوده است، آموزشها زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای دروس علوم و مهندسی کامپیوتر
- زبان برنامه نویسی Go — راهنمای شروع به کار
- چرا باید زبان برنامه نویسی Go را بیاموزیم؟ — راهنمای جامع
- آموزش زبان برنامه نویسی Go: ساخت یک سرور چت به زبان ساده
==