1.13 defer, panic, recovery

1.13 defer, panic, recovery

1.13.1 تعویق (defer) #

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

defer

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

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func main() {
 8	defer fmt.Println("world")
 9	fmt.Println("hello")
10}
1$ go run main.go
2hello
3world

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

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

1package main
2
3import "fmt"
4
5func main() {
6    defer func() { fmt.Println("In inline defer") }()
7    fmt.Println("Executed")
8}
1$ go run main.go
2Executed
3In inline defer

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

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

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

 1package main
 2import "fmt"
 3func main() {
 4    i := 0
 5    i = 1
 6    defer fmt.Println(i)
 7    i = 2
 8    defer fmt.Println(i)
 9    i = 3
10    defer fmt.Println(i)
11}
1$ go run main.go
23
32
41

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

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	i:=1
 7	defer fmt.Println(i)
 8	i++
 9	fmt.Println(i)
10	fmt.Println("First")
11}
1$ go run main.go
22
3First
41

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

1.13.2 پنیک (panic) #

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

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

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

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

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

  • خطای Out of bounds/range array/slice
  • فراخوانی متغیری که nil pointer باشد یعنی به هیچ آدرسی از حافظه memory اشاره نمی‌کند
  • ارسال داده برروی کانال‌های بسته شده
  • type assertion نادرست
 1package main
 2
 3import "fmt"
 4
 5func main() {
 6
 7	a := []string{"a", "b"}
 8	print(a, 2)
 9}
10
11func print(a []string, index int) {
12	fmt.Println(a[index])
13}
1$ go run main.go
2panic: runtime error: index out of range [2] with length 2
3
4goroutine 1 [running]:
5main.checkAndPrint(...)
6        main.go:12
7main.main()
8        /main.go:8 +0x1b
9exit status 2

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

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

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

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

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

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6
 7	a := []string{"a", "b"}
 8	checkAndPrint(a, 2)
 9}
10
11func checkAndPrint(a []string, index int) {
12	if index > (len(a) - 1) {
13		panic("Out of bound access for slice")
14	}
15	fmt.Println(a[index])
16}
1$ go run main.go
2panic: Out of bound access for slice
3
4goroutine 1 [running]:
5main.checkAndPrint(0xc000104f58, 0x2, 0x2, 0x2)
6      main.go:13 +0xe2
7main.main()
8        main.go:8 +0x7d
9exit status 2
توجه کنید استفاده از تابع panic در برخی مواقع مفید می‌باشد. به عنوان مثال قصد دارید هنگام اجرای برنامه، یکسری تنظیمات از سمت کاربر دریافت کنید و در صورتی‌که تنظیمات دارای مشکل بودند، می‌توانید با استفاده panic جلوی ادامه روند برنامه را بگیرید تا کاربر خطا را رفع کند.

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

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

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

1func recover() interface{}

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

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

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6
 7	a := []string{"a", "b"}
 8	checkAndPrint(a, 2)
 9	fmt.Println("Exiting normally")
10}
11
12func checkAndPrint(a []string, index int) {
13	defer handleOutOfBounds()
14	if index > (len(a) - 1) {
15		panic("Out of bound access for slice")
16	}
17	fmt.Println(a[index])
18}
19
20func handleOutOfBounds() {
21	if r := recover(); r != nil {
22		fmt.Println("Recovering from panic:", r)
23	}
24}
1$ go run main.go
2Recovering from panic: Out of bound access for slice
3Exiting normally

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

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

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

 1package main
 2import (
 3    "fmt"
 4    "runtime/debug"
 5)
 6func main() {
 7    a := []string{"a", "b"}
 8    checkAndPrint(a, 2)
 9    fmt.Println("Exiting normally")
10}
11func checkAndPrint(a []string, index int) {
12    defer handleOutOfBounds()
13    if index > (len(a) - 1) {
14        panic("Out of bound access for slice")
15    }
16    fmt.Println(a[index])
17}
18func handleOutOfBounds() {
19    if r := recover(); r != nil {
20        fmt.Println("Recovering from panic:", r)
21        fmt.Println("Stack Trace:")
22        debug.PrintStack()
23    }
24}
 1$ go run main.go
 2Recovering from panic: Out of bound access for slice
 3Stack Trace:
 4goroutine 1 [running]:
 5runtime/debug.Stack(0xd, 0x0, 0x0)
 6        stack.go:24 +0x9d
 7runtime/debug.PrintStack()
 8        stack.go:16 +0x22
 9main.handleOutOfBounds()
10        main.go:27 +0x10f
11panic(0x10ab8c0, 0x10e8f60)
12        /Users/slohia/Documents/goversion/go1.14.1/src/runtime/panic.go:967 +0x166
13main.checkAndPrint(0xc000104f58, 0x2, 0x2, 0x2)
14        main.go:18 +0x111
15main.main()
16        main.go:11 +0x81
17Exiting normally

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


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

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