1.13 defer, panic, recovery

1.13 defer, panic, recovery

1.13.1 تعویق (defer) #

کلمه کلیدی defer یکی از کاربردی‌ترین امکانات زبان گو را برای ما فراهم می‌سازد. شما می‌توانید اجرای یک تابع را به تعویق بندازید‍‍. عموماً defer برای توابعی کاربرد دارد که قصد پاک‌سازی یا بستن عملیات‌های صورت گرفته را دارند، نظیر توابع Close در برخی از جاها.

defer

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

package main

import (
	"fmt"
)

func main() {
	defer fmt.Println("world")
	fmt.Println("hello")
}

1.13.1.1 تعویق (defer) در توابع (Anonymous) #

شما خیلی ساده می‌توانید با استفاده از توابع Anonymous توابع بینام یا گمنام :) اجرای قسمتی از برنامه خودتان را به تعویق بندازید. به مثال زیر توجه کنید:

package main

import "fmt"

func main() {
    defer func() { fmt.Println("In inline defer") }()
    fmt.Println("Executed")
}

به این نکته توجه کنید که defer قبل از return صدا زده می‌شود. یعنی قبل از اینکه تابع شما خروجی را برگشت بدهد اگه تابع خروجی داشته باشه defer اجرا خواهد شد.

1.13.1.2 تعویق (defer) چندین تابع درون یک تابع #

در کد زیر, ما داخل یک تابع چند تابع را با استفاده از (defer) به تعویق انداختیم. به مثال زیر توجه کنید:

package main
import "fmt"
func main() {
    i := 0
    i = 1
    defer fmt.Println(i)
    i = 2
    defer fmt.Println(i)
    i = 3
    defer fmt.Println(i)
}

دقت داشته باشید که مقداردهی پارامترهای ورودی، برای تابعی که آن را defer کردیم در همان لحظه call شدن آن انجام می‌شود. به مثال زیر توجه کنید:

package main

import "fmt"

func main() {
	i:=1
	defer fmt.Println(i)
	i++
	fmt.Println(i)
	fmt.Println("First")
}

در این مرحله شما باید پی برده باشید که defer در همان خطی که نوشته شده است صدا زده می‌شود، ولی اجرای آن دقیقاً به قبل از return در تابع موکول می‌شود.

1.13.2 پنیک (panic) #

در زبان گو panic همانند exception به معنای خروج از برنامه در شرایط غیر عادی است. panic در ۲ حالت زیر پیش می‌آید:

  • خطاهای در زمان اجرای برنامه
  • فراخوانی تابع panic توسط برنامه نویس در بخش های مختلف برنامه
func panic(v interface{})

شما می‌توانید با استفاده از تابع داخلی فوق، panic ایجاد کنید و به عنوان ورودی دلیل panic را در قالب یک رشته به تابع ارسال کنید.

1.13.2.1 خطای panic در زمان اجرا (runtime) #

خطاهای panic در زمان اجرا به دلایل زیر می‌تواند رخ دهد:

  • خطای Out of bounds/range array/slice
  • فراخوانی متغیری که nil pointer باشد یعنی به هیچ آدرسی از حافظه memory اشاره نمی‌کند
  • ارسال داده برروی کانال‌های بسته شده
  • type assertion نادرست
package main

import "fmt"

func main() {

	a := []string{"a", "b"}
	print(a, 2)
}

func print(a []string, index int) {
	fmt.Println(a[index])
}

در تابع فوق ما یک تابع نوشتیم که به عنوان ورودی یک اسلایس از نوع رشته و یک ایندکس از نوع عدد از ما دریافت می‌کند و المنت ایندکسم‌ اون اسلایس را برای ما چاپ میکند در مثال بالا یعنی اندیس شماره 2. این کار ما باعث بروز یک panic میشود, فکر میکنید به چه دلیل ؟ بله به این دلیل که اسلایس ما اندیس شماره 2 ندارد و دلیل آن هم این است که اسلایس, لیست و …. از 0 شروع می‌شوند.

پنیک یک سری اطلاعات در مورد چرایی بوجود آمدنش به ما می‌دهد که در ادامه آن‌ها را توضیح دادیم:

  • پنیک رخ داده شامل متن خطا
  • محل رخ دادن panic در قالب stacktrace

1.13.2.2 خطای panic از قبل تعیین شده توسط برنامه‌نویس #

همانطور که گفتیم شما می‌توانید هرجایی از بدنه توابع خود، تابع panic را فراخوانی کنید البته این روش پیشنهاد نمی‌شود و روش پیشنهادی استفاده از شیوه ارور هندلینگ خود گولنگ است و فقط در صورت لزوم بهتر است از پنیک استفاده شود. همینطور شما باید در داکیومنت برنامه ذکر کنید که کدام قسمت برنامه امکان پنیک را دارد تا دیگران بتوانند در صورت لزوم آن را recover کنند. recover را در ادمه توضیح خواهم داد. تا برنامه در آن محل خطایی را نمایش داده و متوقف شود.

package main

import "fmt"

func main() {

	a := []string{"a", "b"}
	checkAndPrint(a, 2)
}

func checkAndPrint(a []string, index int) {
	if index > (len(a) - 1) {
		panic("Out of bound access for slice")
	}
	fmt.Println(a[index])
}
توجه کنید استفاده از تابع panic در برخی مواقع مفید می‌باشد. به عنوان مثال قصد دارید هنگام اجرای برنامه، یکسری تنظیمات از سمت کاربر دریافت کنید و در صورتی‌که تنظیمات دارای مشکل بودند، می‌توانید با استفاده panic جلوی ادامه روند برنامه را بگیرید تا کاربر خطا را رفع کند.

1.13.3 بازیابی (recovery) #

برخی اوقات panic‌ها غیرقابل پیش‌ بینی می‌شوند. ممکن است برنامه شما بدون هیچ خطایی اجرا شود و به روند خود ادامه دهد، اما این هم ممکن است که به یک دلیل نامعلوم یا بهتر است بگوییم پیش بینی نشده، panic رخ دهد و برنامه شما کاملاً متوقف و باعث از دست دادن وضعیت استیبل برنامه شود.

به همین منظور در گولنگ یک تابع به نام recover وجود دارد که پس از رخ دادن panic در برنامه، این قابلیت را به ما می‌دهد تا بتوانیم برنامه را به وضعیت قبلی خود بازگردانیم تا بعداً خطای panic رخ داده را بررسی و رفع کنیم.

func recover() interface{}

همینطور که شما هم میبینید، تابع ریکاور هیچ ورودی نمی‌گیرد و یک خروجی از تایپ interface را برمی‌گرداند.

به مثالی که در مورد تابع recover زدیم نگاه کنید:

package main

import "fmt"

func main() {

	a := []string{"a", "b"}
	checkAndPrint(a, 2)
	fmt.Println("Exiting normally")
}

func checkAndPrint(a []string, index int) {
	defer handleOutOfBounds()
	if index > (len(a) - 1) {
		panic("Out of bound access for slice")
	}
	fmt.Println(a[index])
}

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

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

1.13.4 چاپ اطلاعات stacktrace پس از بازیابی #

شما می‌توانید پس از اینکه بازیابی را انجام دادید، جزئیات بیشتری در خصوص خطای panic رخ داده بدست آوردید. به مثال زیر توجه کنید:

package main
import (
    "fmt"
    "runtime/debug"
)
func main() {
    a := []string{"a", "b"}
    checkAndPrint(a, 2)
    fmt.Println("Exiting normally")
}
func checkAndPrint(a []string, index int) {
    defer handleOutOfBounds()
    if index > (len(a) - 1) {
        panic("Out of bound access for slice")
    }
    fmt.Println(a[index])
}
func handleOutOfBounds() {
    if r := recover(); r != nil {
        fmt.Println("Recovering from panic:", r)
        fmt.Println("Stack Trace:")
        debug.PrintStack()
    }
}

برای چاپ اطلاعات stacktrace همانطور که می‌بینید ما از پکیج runtime که در کتابخانه استاندارد گولنگ وجود دارد استفاده کردیم

توضیح کوتاه در خصوص stacktrace:

در برنامه نویسی مفهومی به اسم stack trace و یا stack backtrace مطرح است. بصورت خیلی مختصر کاری که انجام می دهد این است مسیر اجرای کد شمارا از نقطه شروع اجرای کد تا زمانی که به اتمام برسد در استک ذخیره میکند. برای مثال زمانی که با یک panic مواجه میشوید شما می توانید مسیری که برنامه از آن عبور کرده تا به panic خورده را مشاهده کنید که این کار با کمک stack trace انجام میشود.