دیدگاه ها
نیازمند احراز هویت
۱۳ مهر ۱۴۰۴
با مفاهیم Defer، Panic و Recover در زبان Go (Golang) آشنا میشویم و یاد میگیریم چطور با استفاده از آنها جریان اجرای برنامه را کنترل کرده و خطاها را بهصورت ایمن مدیریت کنیم.
محتوا و مثال های این پست برگرفته شده از پستی در سایت رسمی گولنگ هست. برای درک بهتر, محتوای اصلی را فارسی سازی کرده و یکسری مثال و توضیحات بیشتر به آن اضافه کرده ایم.
دستور defer
فراخوانی یک تابع را درون یک پشته (stack) قرار میدهد تا پس از اتمام اجرای تابعِ جاری (در زمان بازگشت از آن) اجرا شود. از defer
معمولاً برای اجرای توابعی استفاده میشود که باید در پایان اجرای تابع فراخوانی شوند. توابع اجرا شده با استفاده از دستور defer
معمولاً کار پاکسازی یا بستن ریسورس ها (resources) را به عهده دارند.
ترتیب اجرای آیتمها در پشته بهصورت LIFO (Last In, First Out) است؛ بدین معنی که جدیدترین فانکشنِ اضافه شده به پشته نخستین فانکشنی است که اجرا میشود. در نتیجه، تابع هایی که با دستور defer
فراخوانی میشوند، در انتهای اجرای تابعِ جاری با ترتیبی معکوس اجرا خواهند شد.
به صورت خلاصه تابع defer
یک فانکشن کال را تا زمان اتمام اجرای تابع جاری به تعویق میاندازد.
در مثال زیر محتوای یک فایل را به درون یک فایل دیگر کپی میکنیم:
در مثال بالا یک باگ وجود دارد زیرا در صورتی که تابع os.Create
با خطا مواجه شود, ریسورس مرتبط با تابع os.Open
بسته نخواهد شد. در مثال بالا با قرار دادن src.Close()
قبل از return
درون if
دوم میتوان این مشکل را حل کرد اما در حالت هایی که منطق تابع پیچیده تر باشد پیدا کردن و حل این مدل مشکلات سخت خواهد بود.
تابع بالا را میتوان با استفاده از دستور defer
به صورت زیر بازنویسی کرد:
دستور defer
این امکان را به ما میدهد تا کد مربوط به بستن هر فایل را بلافاصله بعد از کد مرتبط با باز کردن فایل قرار دهیم. به این ترتیب، صرف نظر از تعداد و موقعیت عبارتهای return
در تابع، اطمینان حاصل میشود که تمام فایلها در پایان اجرای تابع بهدرستی بسته خواهند شد.
رفتار دستور defer
خیلی ساده است. برای اینکه درک خوبی از دستور defer
داشته باشیم باید نکاتی که در زیر بهشون اشاره میکنیم رو بدونیم:
آرگومانهای تابعی که با defer
فراخوانی میشود، در همان لحظهای ارزیابی میشوند که دستور defer
اجرا میگردد (نه در زمان اجرای تابعی به تعویق افتاده است).
در مثال بالا در هنگام فراخوانی تابع fmt.Println
با استفاده از دستور defer
مقدار i
برابر با 0
است. بنابراین مقدار 0
در خروجی استاندارد چاپ خواهد شد.
توابعی که با استفاده از defer
فراخوانی شده اند، پس از بازگشت تابع جاری با ترتیبی معکوس نسبت به زمان فراخوانی یعنی به صورت آخر به اول یا LIFO (Last In, First Out) اجرا میشوند.
در مثال بالا مقدار 3210
در خروجی استاندارد چاپ خواهد شد.
توابعی که با defer
فراخوانی میشوند، میتوانند به مقادیرِ بازگشتیِ نامگذاری شدهیِ تابع دسترسی داشته باشند در صورت نیاز مقدار آنها را تغییر دهند.
در مثال بالا فانکشنی که با استفاده از defer
فراخوانی کردیم, دقیقا قبل از بازگشت تابع, مقدار i
را با یک جمع میکند و بدین صورت مقدار 2
توسط تابع برگردانده و درون خروجی استاندارد (stdout) چاپ میشود. در گولنگ از این ویژگی میتوان برای تغییر خطای (error) بازگشتی از توابع نیز استفاده کرد.
تابع panic
یک تابع داخلی (built-in) در Go است که جریان عادی اجرای برنامه را متوقف میکند و در صورت عدم کنترل باعث کرش کردن (crash) برنامه می شود.
زمانیکه تابعی مانند F
، تابع panic
را فراخوانی کند، اجرای F
متوقف میشود؛ اما تمام توابعی که با دستور defer
در F
ثبت شدهاند، بهصورت معمول اجرا میگردند. پس از آن، تابع F
به فراخوانندهی خود بازمیگردد و برای آن، همانند یک فراخوانی مستقیم به panic
رفتار میکند. این روند بهصورت پیاپی در طول پشتهی فراخوانی (call stack) ادامه مییابد تا زمانیکه تمامی توابع در گوروتین (goroutine) جاری خاتمه یابند؛ در این مرحله، برنامه متوقف شده و دچار کرش (crash) میشود.
حالت panic
میتواند بهطور مستقیم با فراخوانی تابع panic
آغاز شود، یا بهطور غیرمستقیم در نتیجهی بروز خطاهای زمان اجرا (runtime errors) مانند دسترسی خارج از محدودهی آرایه (out-of-bounds array access) رخ دهد.
تابع recover
یک تابع داخلی (built-in) در Go است که به ما این قابلیت را میدهد تا کنترل اجرای یک گوروتین (goroutine) در حال panic را مجدداً بهدست بگیریم. تابع recover
تنها درون توابعی که با دستور defer
فراخوانی شدهاند مفید است و در خارج از آنها کاربردی ندارد.
در جریان اجرای عادی برنامه، فراخوانی recover
مقدار nil
را بازمیگرداند و هیچ تأثیر دیگری ندارد. اما اگر گوروتین (goroutine) فعلی در حالت panic
باشد، فراخوانی recover
مقدار دادهشده به panic
را دریافت کرده و اجرای عادی برنامه را از سر میگیرد.
در مثال بالا تابع g
یک عدد صحیح (int) بهنام i
را دریافت میکند و در صورتیکه مقدار i
بزرگتر از 3
باشد، وارد حالت panic
میشود؛ در غیر این صورت، خود را با آرگومان i+1
مجدداً فراخوانی میکند. تابع f
نیز تابعی را با دستور defer
بهتعویق میاندازد که در زمان اجرا، recover
را فراخوانی کرده و در صورت غیر nil
بودن مقدار بازگردانده شده، آن را چاپ میکند. با توجه به این توضیحات خروجی تابع بالا همانند زیر خواهد بود:
اگر تابعی که با defer
اجرا کرده ایم را از تابع f
حذف کنیم، حالت panic
بازیابی نخواهد شد و تا بالاترین سطح پشتهی فراخوانی (call stack) در گوروتین گسترش مییابد. در نتیجه، برنامه کرش کرده و اجرای آن خاتمه مییابد. خروجی نسخهی اصلاحشدهی این برنامه بهصورت زیر خواهد بود:
قسمت قبل: جنریک ها (generics) | گولنگ به زبان ساده
قسمت بعد: جیسان (JSON) | گولنگ به زبان ساده