الكتاب الأساسي

بعض حالات استخدام panic/recover

قُدِّم كل من panic وrecover سابقاً. سيقدّم ما يلي من هذا المقال بعض حالات استخدام panic/recover (الجيدة والسيئة).

حالة الاستخدام 1: تجنّب انهيار البرامج بسبب الـ panics

ينبغي أن تكون هذه أشهر حالة استخدام لـ panic/recover. وتُستخدم هذه الحالة كثيراً في البرامج المتزامنة، وخاصة برامج العميل-الخادم.

مثال:


package main

import "errors"
import "log"
import "net"

func main() {
	listener, err := net.Listen("tcp", ":12345")
	if err != nil {
		log.Fatalln(err)
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println(err)
		}
		// معالجة كل اتصال عميل
		// في goroutine جديدة.
		go ClientHandler(conn)
	}
}

func ClientHandler(c net.Conn) {
	defer func() {
		if v := recover(); v != nil {
			log.Println("capture a panic:", v)
			log.Println("avoid crashing the program")
		}
		c.Close()
	}()
	panic(errors.New("just a demo.")) // panic بغرض العرض التوضيحي
}

ابدأ تشغيل الخادم ونفّذ telnet localhost 12345 في طرفية أخرى، فيمكننا ملاحظة أن الخادم لن ينهار بسبب الـ panics الناشئة في كل goroutine من goroutines معالجة العملاء.

إذا لم نستعد (recover) من الـ panic المحتمل في كل goroutine من goroutines معالجة العملاء، فسيؤدي ذلك الـ panic المحتمل إلى انهيار البرنامج.

حالة الاستخدام 2: إعادة تشغيل goroutine منهارة تلقائياً

عندما يُكتشف panic في goroutine، يمكننا إنشاء goroutine جديدة لها. مثال:


package main

import "log"
import "time"

func shouldNotExit() {
	for {
		// محاكاة عبء عمل.
		time.Sleep(time.Second)

		// محاكاة panic غير متوقعة.
		if time.Now().UnixNano() & 0x3 == 0 {
			panic("unexpected situation")
		}
	}
}

func NeverExit(name string, f func()) {
	defer func() {
		if v := recover(); v != nil {
			// اكتُشفت panic.
			log.Println(name, "is crashed. Restart it now.")
			go NeverExit(name, f) // إعادة التشغيل
		}
	}()
	f()
}

func main() {
	log.SetFlags(0)
	go NeverExit("job#A", shouldNotExit)
	go NeverExit("job#B", shouldNotExit)
	select{} // انحباس هنا إلى الأبد
}

حالة الاستخدام 3: استخدام نداءات panic/recover لمحاكاة جملات القفز الطويل

أحياناً، يمكننا استخدام panic/recover كطريقة لمحاكاة جملات القفز الطويل العابرة للدوال والإرجاعات العابرة للدوال، مع أن استخدام هذه الطريقة غير مُستحسن عموماً. تضرّ هذه الطريقة بكلٍّ من قابلية قراءة الشيفرة وكفاءة التنفيذ. والفائدة الوحيدة أنها قد تجعل الشيفرة تبدو أقل إطناباً في بعض الأحيان.

في المثال التالي، بمجرد نشوء panic في دالة داخلية، سينتقل التنفيذ إلى النداء المؤجَّل.


package main

import "fmt"

func main() {
	n := func () (result int)  {
		defer func() {
			if v := recover(); v != nil {
				if n, ok := v.(int); ok {
					result = n
				}
			}
		}()

		func () {
			func () {
				func () {
					// ...
					panic(123) // panic عند النجاح
				}()
				// ...
			}()
		}()
		// ...
		return 0
	}()
	fmt.Println(n) // 123
}

حالة الاستخدام 4: استخدام نداءات panic/recover لتقليل فحوص الأخطاء

مثال:


func doSomething() (err error) {
	defer func() {
		switch e := recover().(type) {
		case nil:
		case error:
			err = e
		default:
			panic(e) // إعادة إطلاق الـ panic
		}
	}()

	doStep1()
	doStep2()
	doStep3()
	doStep4()
	doStep5()

	return
}

// في الواقع، قد تكون نماذج دوال doStepN
// مختلفة. هنا، لكل واحدة منها،
// * panic بقيمة nil عند النجاح وعدم الحاجة إلى المتابعة.
// * panic بقيمة error عند الفشل وعدم الحاجة إلى المتابعة.
// * عدم إحداث panic للمتابعة.
func doStepN() {
	...
	if err != nil {
		panic(err)
	}
	...
	if done {
		panic(nil)
	}
}

الشيفرة أعلاه أقل إطناباً من الشيفرة التالية.


func doSomething() (err error) {
	shouldContinue, err := doStep1()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep2()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep3()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep4()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep5()
	if !shouldContinue {
		return err
	}

	return
}

// إذا لم تكن err مساوية لـ nil، فيجب أن تكون shouldContinue صحيحة.
// إذا كانت shouldContinue صحيحة، فقد تكون err مساوية لـ nil أو غير مساوية له.
func doStepN() (shouldContinue bool, err error) {
	...
	if err != nil {
		return false, err
	}
	...
	if done {
		return false, nil
	}
	return true, nil
}

مع ذلك، عادةً لا يُستحسن استخدام نمط panic/recover هذا. فهو أقل انسجاماً مع أسلوب Go وأقل كفاءة.

ولاحظ من فضلك أنه منذ Go 1.21، أصبح نداء panic(nil) مكافئاً لـ panic(new(runtime.PanicNilError)). لذا ينبغي منذ Go 1.21 كتابة نداء الدالة المؤجَّل أعلاه على النحو التالي


func doSomething() (err error) {
	defer func() {
		switch e := recover().(type) {
		case nil, *runtime.PanicNilError:
		case error:
			err = e
		default:
			panic(e) // إعادة إطلاق الـ panic
		}
	}()

	doStep1()
	...
}