مهدی اکبری

توسعه دهنده

درباره defer در گو

2023-06-11

قبل از هرچیز، بیایید ببینیم defer به طور خلاصه چی هست؟
دیفر میاد زمان فراخوانی شدن عبارت جلوی خودش رو به قبل جایی که تابع دربرگیرندش چیزی برگردونه موکول میکنه.

یعنی چی؟ این کد رو ببنید.



func main() {
    defer fmt.Println("Hey I'm defered.")
    fmt.Println("Hello World.")
}


ما اگر اینو اجرا کنیم، خروجی زیر رو میده:



Hello World.
Hey I'm defered.


مشاهده کردید که اول سلام دنیا پرینت شد و بعد متن دیفر شده.


خب حالا که کاربرد خیلی ساده و نحوه کارکردش رو دیدید. یه سری نکات هست که باید بدونیم.



۱. ارزیابی آرگومان ها

اسم آرگومان رو اوردم مجبورم تفاوت آرگومان با پارامتر رو بگم، پارامتر متغییری هستش که جزوی از امضای تابع/متود ما هستش اما آرگومان عباراتی هستن که ما موقع استفاده از تابع/متود بهش میدیم.



func Foo(a int, b int) {
    // Do something
}

func main() {
    anInt := 1
    Foo(anInt, 5)
}


برای مثال در اینجا a و b پارامتر محسوب میشن و anInt و 5 آرگومان محسوب میشن.

حالا می‌دونیم آرگومان چیه. آرگومان هایی که به تابع دیفر شده، داده میشن. دقیقا همون زمانی که به خود defer می‌رسه ارزیابی میشه. نه وقتی که تابع جلوش فراخوانی میشه.



func main() {
 i := 0
 defer fmt.Println(i)
 i++
}


با اینکه fmt.Println(i) اخرین چیزیه که اجرا میشه. اما همچنان 0 پرینت میشه.

چطوری درستش کنیم؟
کافیه که آرگومان هارو ببریم داخل بدنه تابع دیفر شده.



func main() {
 i := 0
 defer func() {
  fmt.Println(i)
 }()
 i++
}


اینجوری 1 پرینت میشه.



۲. ارزیابی متودها

اون پستی که من دیدم راجب دیفر گو، اومده بود نوشته بود: Evaluating Receiver Functions در واقع درست تر هست که بهشون بگیم توابع دریافت کننده اما چون احتمالا با مفهوم شی گرایی آشنا هستید و هستیم، از تشابه اومدن استفاده کردن و با این اسم متود پیش می‌ریم، گفتم توابع دریافت کننده بنظرم اسم بهتریه از اونجا که گو بر پایه زبان شی‌گرایی محسوب نمیشه و وقتی هم که میاییم این توابع رو تعریف کنیم بجای اینکه توی پارامتر تایپ رو بهش بدیم، قبل از تعریف اسم تابع نوع متغییر رو بهش می‌دیم.



type Person struct {
 Name string
}

func (p Person) SayHi() {
 fmt.Println("Hi, my name is", p.Name)
}

func main() {
 writer := Person{Name: "Jhon"}
 defer writer.SayHi()

 // change the name
 writer.Name = "Sam"
}


الان SayHi تابع دریافت کننده‌ست چرا که تایپ Person رو با نام p دریافت کرده.
به بیان دیگه SayHi متودی روی هر متغییری هست که تایپش Person باشه.


حالا. اینجا هم مثل قسمت ۱ میبینیم که خروجی به جای اینکه اسم جدید تحویل بده همون اسم قبلی یعنی جان رو تحویل میده.


مقدار writer در زمانی مورد ارزیابی قرار می‌گیره که ما میرسیم به استیتمنت defer و ازونجایی که متود SayHi یک دریافت کننده مقدار هستش(پوینتر دریافت نکرده برای Person)، بنابرین میاد یه کپی از writer می‌گیره و وقتی تابع خواست خارج بشه یا چیزی برگردونه متود SayHi روشو فراخوانی می‌کنه.


از اونجاییم که کپی از رایتر گرفته بود بنابرین تغییری که ما روی رایتر انجام میدیم روی رایتری که دست defer هست اعمال نمیشه.


اینم فیکس شدنیه؟
بله، کافیه از پوینتر روی تابع SayHiمون استفاده کنیم. اینجوری تغییری که صورت می‌گیره موقع فراخوانی SayHi اسم جدید رو پرینت می‌کنه. منظورم از فراخوانی همون call کردن هستش.


متود اپدیت شده اینطوری میشه:



func (p *Person) SayHi() {
 fmt.Println("Hi, my name is", p.Name)
}



۳. نگران panic نباشید، defer همچنان فراخوانی میشه

دیفر اجرا میشه حتی اگر تابع panic کنه. این قابلیت وقتی مفید واقع میشه که مثلا می‌خوایم فایل رو ببندیم یا تسک هارو کلین آپ کنیم یا پایگاه داده رو ببندیم یا قفل mutex رو باز کنیم.


برای اینکه از پایین رفتن سرویس جلوگیری کنیم معمولا میایم از ترکیب defer و recover استفاده می‌کنیم که بتونیم وضعیت های استثنایی که انتظارشو نداریم هندل کنیم.



func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()

    panic("This is a panic!")
}

// Recovered from panic: This is a panic!


این دلیلی هستش که بسیاری از کتابخونه‌های مربوط به وب سرور (مثلا Gin) یه میان‌افزار(middleware) مربوط به recover کردن دارن که از همین تکنیک استفاده می‌کنه.



۴. دیفر می‌تونه خروجی تابع رو عوض کنه

اگر از مقادیر بازگشتی نام‌گذاری شده(named return values) استفاده کنید، مثل مثال زیر:



func deferMe() (x int) {
    x = 1
    return
}


با حقه defer می‌تونید خروجی یه تابع رو قبل اینکه return کنه، تغییر بدید، به صورت زیر:



func main() {
  fmt.Println(deferMe()) // 2
}

func deferMe() (x int) {
  defer func() { x *= 2 }()

  x = 1
  
  return
}


یادتون باشه که استفاده از این روش باعث میشه خوندن/فهمیدن/دیباگ کردن کدتون کمی سخت تر بشه، فقط وقتی ضروری هستش از این روش استفاده کنید.



۵. ترتیب defer - اخری داخل، اولی بیرون

منظورمون از اخری داخل، اولی بیرون چیه؟ LIFO = Last in First out
یعنی آخرین چیزی که وارد شده، اولین چیزیه که خارج میشه. ساختمان داده رو که یادتون هست؟
دیفر میاد عباراتی که باید اجرا بشن رو میریزه توی یه پشته(stack).


بیایید با مثال ببینید که متوجه شید:



func main() {
    defer fmt.Println("Third")
    defer fmt.Println("Second")
    defer fmt.Println("First")

    fmt.Println("Hello World.")
}


و اگر خروجی رو مشاهده کنیم، می‌بینیم:



Hello World.
First
Second
Third



امیدوارم سلامت و تندرست باشید و از این مطلب لذت برده باشید، بدرود.



اگر از این پست لذت بردید می‌تونید دوره‌ی ساخت پلتفرم آگهی خودروی من با زبان گو رو روی یوتوب ببینید:
لینک مربوطه