1 معرفی ویژگی defer
در زبان برنامهنویسی گولنگ (Golang)
در زبان برنامهنویسی گولنگ، دستور defer
اجرای فراخوانی تابعی که پس از آن آمده است را تا زمانی که تابعی که دستور defer
در آن آمده است به پایان رسیده، به تأخیر میاندازد. میتوانید آن را به عنوان بلوک finally
در سایر زبانهای برنامهنویسی تصور کنید، اما استفاده از defer
انعطافپذیرتر و منحصربهفردتر است.
مزیت استفاده از defer
این است که میتوان از آن برای انجام وظایف پاکسازی مانند بستن فایلها، آزادسازی mutexها یا فقط ثبت زمان خروجی تابع استفاده کرد. این میتواند باعث افزایش ایمنی برنامه و کاهش تعداد کدهای برنامهنویسی مربوط به پردازش استثنا شود. در فلسفه طراحی گولنگ، استفاده از defer
توصیه میشود زیرا باعث حفظ کدهای مختصر و خوانا برای مدیریت خطاها، پاکسازی منابع و دیگر عملیاتهای پساپردازشی میشود.
2 اصل کار defer
2.1 اصل کار ابتدایی
اصل کار ابتدایی defer
استفاده از یک استک (بر اساس اصل آخرین وارد شده، اولین خارج میشود) برای ذخیره هر تابع مأخرف که باید اجرا شود است. هنگامی که یک دستور defer
ظاهر میشود، زبان برنامهنویسی گولنگ تابعی که پس از دستور آمده است را فوراً اجرا نمیکند. به جای آن، آن را به یک استک اختصاصی فشرده میکند. تنها زمانی که تابع بیرونی در حالی که در حال خروج است به پایان میرسد، این توابع مأخرف اجرا خواهند شد به ترتیب استک، به گونهای که تابعی که در آخرین دستور defer
اعلام شده است، اولین بار اجرا میشود.
علاوه بر این، ارزش توجه به آن است که پارامترهای توابع در ادامه دستور defer
در زمان اعلام دستور defer
محاسبه و تثبیت میشوند وقتی که دستور defer
اعلام میشود، نه در زمان اجرای واقعی.
func example() {
defer fmt.Println("جهان") // مأخرف شده
fmt.Println("سلام")
}
func main() {
example()
}
کد فوق خروجی زیر را به نمایش میگذارد:
سلام
جهان
جهان
پیش از خروجی دادن تابع example
چاپ میشود، حتی اگر ظاهر میشود که پردازش به توالی سلام
را به تأخیر میاندازد.
2.2 ترتیب اجرای چندین دستور defer
زمانی که یک تابع چندین دستور defer
دارد، آنها به ترتیب آخرین وارد شده، اولین خارج میشوند اجرا میشوند. این امر اغلب برای درک منطق پاکسازی پیچیده بسیار اهمیت دارد. مثال زیر اجرای چندین دستور defer
را نشان میدهد:
func multipleDefers() {
defer fmt.Println("اولین دستور defer")
defer fmt.Println("دومین دستور defer")
defer fmt.Println("سومین دستور defer")
fmt.Println("بدنه تابع")
}
func main() {
multipleDefers()
}
خروجی این کد به صورت زیر است:
بدنه تابع
سومین دستور defer
دومین دستور defer
اولین دستور defer
از آنجا که defer
از اصل آخرین وارد شده، اولین خارج میشود پیروی میکند، حتی اگر "اولین دستور defer" اولین مأخرف شده باشد، آخرین اجرا میشود.
3 کاربردهای defer
در سناریوهای مختلف
3.1 آزادسازی منابع
در زبان برنامهنویسی گولنگ، دستور defer
معمولاً برای مدیریت منطق آزادسازی منابع مانند عملیات فایل و اتصالات پایگاه داده استفاده میشود. defer
اطمینان میدهد که پس از اجرای تابع، منابع مربوطه به درستی آزاد میشوند بدون توجه به دلیل خروج از تابع.
مثال عملیات فایل:
func ReadFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// از `defer` برای اطمینان از بستن فایل استفاده میکنیم
defer file.Close()
// انجام عملیات خواندن فایل...
}
در این مثال، بعد از اینکه os.Open
به موفقیت فایل را باز میکند، دستور defer file.Close()
اطمینان میدهد که منبع فایل به درستی بسته و منبع دستگیره فایل آزاد میشود هنگامی که تابع به پایان میرسد.
مثال اتصال پایگاه داده:
func QueryDatabase(query string) {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
// اطمینان از بستن اتصال پایگاه داده با استفاده از `defer`
defer db.Close()
// انجام عملیات پرس و جوی پایگاه داده...
}
به همین ترتیب، defer db.Close()
اطمینان میدهد که اتصال پایگاه داده هنگام خروج از تابع QueryDatabase
به درستی بسته میشود، بدون توجه به دلیل (بازگشت نرمال یا انداختن استثنا).
3.2 عملیات قفلگذاری در برنامهنویسی همروند
در برنامهنویسی همروند، استفاده از defer
برای مدیریت آزادسازی قفل mutex عملیات خوبی است. این امر اطمینان میدهد که قفل پس از اجرای صحیح بخش مهم کد به درستی آزاد میشود و از deadlockها جلوگیری میکند.
مثال برای قفل (Lock) Mutex:
var mutex sync.Mutex
func updateSharedResource() {
mutex.Lock()
// از دیفر به منظور اطمینان از آزادسازی قفل استفاده میکنیم
defer mutex.Unlock()
// انجام تغییرات در منبع مشترک...
}
بدون توجه به این که تغییر منبع مشترک موفق باشد یا یک حالت ناگواری در بین رخ دهد، defer
اطمینان میدهد که Unlock()
صدا زده شود و اجازه میدهد تا گوروتینهای دیگری که منتظر قفل هستند، آنرا بگیرند.
نکته: توضیحات دقیق در مورد قفل Mutex در فصول بعدی بررسی خواهد شد. در حال حاضر، درک حالات کاربردی
defer
کافی است.
3 مشکل متداول و نکات توجه در استفاده از defer
در استفاده از defer
، علیرغم بهبود خوانایی و قابلیت تعمیر کد، برخی مشکلات و نکات توجه وجود دارد که باید به آنها توجه داشت.
3.1 ارزیابی فوری پارامترهای تأخیری (deferred)
func printValue(v int) {
fmt.Println("مقدار:", v)
}
func main() {
value := 1
defer printValue(value)
// تغییر مقدار `value`، بر پارامتر از پیش گذاشته شده به defer تأثیر نخواهد گذاشت
value = 2
}
// خروجی "مقدار: 1" خواهد بود
با وجود تغییر مقدار value
پس از اظهاریه defer
، پارامتر از پیش گذاشته شده به printValue
در defer
ارزیابی شده و ثابت است، بنابراین خروجی همچنان "مقدار: 1" خواهد بود.
3.2 مواظبت در استفاده از defer درون حلقهها
استفاده از defer
داخل حلقه ممکن است منجر به عدم آزادسازی منابع قبل از پایان حلقه شود که میتواند منجر به نشت یا خالی شدن منابع شود.
3.3 اجتناب از "آزادسازی پس از استفاده" در برنامهنویسی همروند
در برنامههای همروند، در استفاده از defer
برای آزادسازی منابع، اهمیت دارد که اطمینان حاصل شود که تمامی گوروتینها تلاش نخواهند کرد تا به منبع دسترسی پیدا کنند پس از آن آزاد شده باشد تا از بروز وضعیتهای رقابتی جلوگیری شود.
4. توجه به ترتیب اجرای دستورات defer
دستورات defer
به اصل آخر وارد شده به اصل اوّل (LIFO) پیروی میکنند که بهاین معنی است که آخرین defer
اعلام شده، ابتدا اجرا میشود.
راهحلها و بهترین روشها:
- همواره به این موضوع توجه کنید که پارامترهای تأخیری (deferred) در دستورات
defer
در زمان اظهاریه ارزیابی میشوند. - در استفاده از
defer
داخل یک حلقه، در نظر داشته باشید از توابع ناشناس یا فراخوانی صریح آزادسازی منابع استفاده کنید. - در محیطهای همروند، اطمینان حاصل کنید که تمامی گوروتینها عملیات خود را به پایان رساندهاند قبل از استفاده از
defer
برای آزادسازی منابع. - در هنگام نوشتن توابع حاوی چندین دستور
defer
، با دقت به ترتیب اجرا و منطق آنها توجه کنید.
رعایت این بهترین روشها میتواند از بیشتر مشکلات مواجهه شده در استفاده از defer
جلوگیری کرده و باعث نوشتن کد Go قویتر و قابل دسترس میشود.