۱۳ مهر ۱۴۰۴
با مفاهیم 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) | گولنگ به زبان ساده