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 قوی‌تر و قابل دسترس می‌شود.