خلاص شدن از دست چک کردن خطای توابع defer شده

خیلی وقت ها پیش میاد که لازم باشه خطای function callی رو بررسی کنیم که defer شده. مثلا اگه داریم از http.Client برای http request استفاده میکنیم وقتی که response رو دریافت می کنیم باید resp.Body.Close رو فراخوانی کنیم.

func request(url string) error {
	resp, err := http.Get(url)
	defer resp.Body.Close()

	doSomething()
}

اما این فراخوانی باید همراه با defer باشه که در هنگام خاتمه یافتن کار تابع صورت بگیره.
مشکل وقتی بیشتر میشه که لازم باشه خطای تابع Close رو هم بررسی کنیم. یعنی هم لازم باشه defer کنیم و هم لازم باشه خطای فراخوانی تابع resp.Body.Close رو بررسی کنیم. احتمالا چنین کدی مینویسیم:

func request(url string) error {
	resp, err := http.Get(url)
	defer func() {
		cErr := resp.Body.Close()
  		if cErr != nil {
    		fmt.Println("oops resp.Body.Close error", cErr)
  		}
	}()

	doSomething()

	return nil
}

خب این کار میتونه طاقت فرسا بشه اگه تعداد دفعات تکرار این شرایط زیاد باشه. ضمنا شاید بهتر باشه به پرینت کردن خطای cErr بسنده نکنیم و واقعا خطای cErr رو به عنوان خطای تابع request برگردونیم.
در چنین شرایطی شاید بد نباشه یه helper function بنویسیم، به این صورت:

func CaptureDefErr(f func() error, err *error) {
	if f == nil {
		return
	}

	cErr := f()

	if err == nil {
		return
	}

	// if both cErr and *err isn't nil, we should log cErr, or append cErr to *err, or wrap it...
	if *err == nil {
		*err = cErr
	}
}

func request(url string) (rErr error) {
	resp, err := http.Get(url)
	if err != nil {
		rErr = err
		return
	}
	
	defer CaptureDefErr(resp.Body.Close, &rErr)

	doSomething()

	return rErr
}

خب اینطوری ما خیالمون راحت میشه که خطای defer شده به عنوان خطای خروجی تابع اصلی برمیگرده. و فراخوانی تابع defer هم خیلی راحت تره. اما مثل بقیه مسائل برنامه نویسی، هر تصمیمی، علاوه بر خوبی هاش یه سری هزینه و بدی هم داره. شاید مهم ترین بدی این helper function این باشه که ما مجبور میشیم که از named return value برای rErr استفاده کنیم که میتونه خودش error prone و باگخیز باشه! ولی در کل انتخابش با خودتونه. اگه بچه های خوب و حواس جمعی باشید میتونید ازش استفاده کنید و فکر میکنم کار راه بندازه.

2 پسندیده

ارور ها بهتره به توابع بالاتر پاس داده بشن (caller) و اونجا هندل بشن. اگه اینجا توی تابع request ارور رو با فراخواندن captureDefErr هندل کنیم نباید در لایه های بالاتر دوباره هندل بشه مگر اینکه لاجیک های مختلف بخواد اجرا بشه توی هر لایه!
مثلا در اینجا Body.Close شده که کاملا صحیح هست.