اذهب إلى المحتوى

معالجة حالات الانهيار في لغة جو Go


هدى جبور

تنقسم الأخطاء التي قد تحدث في البرنامج إلى فئتين رئيسيتين هما أخطاء يتوقع المبرمج حدوثها وأخطاء لم يتوقع حدوثها، وتُعالِج الواجهة error التي تحدّثنا عنها في المقال السابق إلى حد كبير الأخطاء التي نتوقعها أثناء كتابة البرامج حتى تلك الأخطاء التي تكون احتمالات حدوثها نادرةً.

تندرج حالات الانهيار Panics تحت الفئة الثانية والتي تؤدي إلى إنهاء البرنامج تلقائيًا والخروج منه، وعادةً تكون الأخطاء الشائعة هي سبب حالات الانهيار، لذا سندرس في هذا المقال بعض الحالات الشائعة التي يمكن أن تؤدي إلى حالات الانهيار، كما سنلقي نظرةً أيضًا على بعض الطرق التي يُمكن أن نتجنب من خلالها حالات الانهيار، وسنستخدم أيضًا تعليمات التأجيل defer statements جنبًا إلى جنب مع الدالة recover لالتقاط حالات الانهيار قبل أن تؤدي إلى إيقاف البرنامج.

ما هي حالات الانهيار؟

هناك عمليات محددة في لغة جو تؤدي إلى حالات الانهيار ومن ثم إيقاف البرنامج مباشرةً، وتتمثّل بعض هذه العمليات في محاولة الوصول إلى فهرس لا تتضمنه حدود المصفوفة -أي تجاوز حدود المصفوفة- أو إجراء عمليات توكيد النوع type assertions أو استدعاء توابع على مؤشرات لا تُشير إلى أيّ شيء nil أو استخدام كائنات المزامنة mutexes بطريقة خاطئة أو محاولة العمل مع القنوات المغلقة closed channels، فمعظم هذه المواقف تنتج عن أخطاء أثناء البرمجة ولا يستطيع المُصرِّف اكتشافها أثناء تصريف البرنامج.

بما أن حالات الانهيار تتضمن تفاصيل مفيدةً لحل مشكلة ما، فعادةً ما يستخدِم المطورون حالات الانهيار بوصفها مؤشرًا على ارتكابهم خطأ أثناء تطوير البرنامج.

حالات الانهيار الناتجة عن تجاوز الحدود

ستولد جو حالة انهيار في وقت التشغيل runtime عند محاولة الوصول إلى فهرس خارج حدود المصفوفة أو الشريحة، ويجسِّد المثال التالي هكذا حالة، إذ سنحاول الوصول إلى آخر عنصر من الشريحة من خلال القيمة المُعادة من الدالة len، أي من خلال طول الشريحة.

package main

import (
    "fmt"
)

func main() {
    names := []string{
        "lobster",
        "sea urchin",
        "sea cucumber",
    }
    fmt.Println("My favorite sea creature is:", names[len(names)])
}

يكون الخرج كما يلي:

panic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

لاحظ أننا حصلنا على تلميح panic: runtime error: index out of range والذي يشير إلى أننا نحاول الوصول إلى فهرس خارج حدود المصفوفة.

أنشأنا شريحة names بثلاث قيم ثم حاولنا طباعة العنصر الأخير من خلال القيمة المُعادة من الدالة ()len والتي ستُعيد القيمة 3، لكن آخر عنصر في المصفوفة يملك الفهرس 2 وليس 3 بما أنّ الفهرسة تبدأ من الصفر وليس من الواحد، لذا في هذه الحالة لا يوجد خيارًا لوقت التشغيل إلا أن يتوقف ويُنهي البرنامج، أيضًا لا يمكن لجو أن تُبرهِن أثناء عملية التصريف أن الشيفرة ستحاول فعل ذلك، لذلك لا يمكن للمُصرِّف التقاط هذا.

تُسبب حالة الانهيار إيقاف تنفيذ برنامجك تمامًا، وبالتالي لن تُنفّذ أيّة تعليمات برمجية تالية، كما تحتوي الرسالة الناتجة على العديد من المعلومات المفيدة في تشخيص سبب حالة الانهيار.

مكونات حالة الانهيار

تتكون حالات الانهيار من رسالة تحاول توضيح سبب حالة الانهيار إضافةً إلى مسار المكدس stack trace الذي يساعدك على تحديد مكان حدوث حالة الانهيار في تعليماتك البرمجية، إذ تبدأ رسالة الانهيار بالكلمة panic:‎ تتبعها سلسلة نصية توضيحية تختلف تبعًا لسبب حالة الانهيار، فرسالة الانهيار في المثال السابق كانت كما يلي:

panic: runtime error: index out of range [3] with length 3

تخبرنا السلسلة runtime error:‎ التي تتبع الكلمة panic:‎ أنّ الخطأ قد حدث في وقت التشغيل؛ أما بقية الرسالة، فتخبرنا بأن الفهرس 3 خارج حدود الشريحة.

الرسالة التالية هي رسالة مسار المكدس، وهي خريطة يمكننا تتبُعها لتحديد سطر التعليمات البرمجية الذي أدى تنفيذه إلى حدوث حالة الانهيار وكيفية استدعاء هذه الشيفرة من شيفرة أخرى.

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

يوضّح مسار المكدس هذا أن حالة الانهيار قد حدثت في الملف ‎/tmp/sandbox879828148/prog.go في السطر رقم 13، كما يخبرنا أيضًا أنه نشأ في الدالة ()main من الحزمة الرئيسية main.

ينقسم مسار المكدس إلى عدة أقسام بحيث يكون هناك قسم لكل روتين goroutine فكل عملية تنفيذ لبرنامج في جو تُنجز من خلال روتين واحد أو أكثر، أي كل روتين يُنفذ جزء محدد من الشيفرة، وكل من هذه الروتينات يُمكن تنفيذها باستقلالية عن باقي الروتينات وبالتزامن معها.

تبدأ كل كتلة بالرأس :[goroutine X [state، تُمثّل الإشارة X إلى رقم الروتين ID (مُعرّفه) مع الحالة التي كان عليها عندما حدثت حالة الانهيار، ويعرض مسار المكدس بعد الرأس الدالة التي حدثت فيها حالة الانهيار أثناء تنفيذها إلى جانب اسم الملف ورقم سطر تنفيذ الدالة.

الإشارة إلى العدم

يمكن أن تحدث حالة الانهيار أيضًا عند محاولة استدعاء تابع على مؤشر لا يُشير إلى أيّ شيء، إذ تُمكننا المؤشرات في جو من الإشارة إلى نسخ مُحددة من بعض الأنواع الموجودة في الذاكرة خلال وقت التشغيل، وعندما لا يُشير المؤشر إلى أيّ شيء، فستكون قيمة المؤشر nil، وعندما نحاول استدعاء توابع على مؤشر لا يشير إلى شيء، فستظهر حالة انهيار، والأمر ذاته بالنسبة للمتغيرات التي تكون من أنواع الواجهات، إذ تولّد أيضًا حالة الانهيار إذا استُدعيت توابع عليها كما في المثال التالي:

package main

import (
    "fmt"
)

type Shark struct {
    Name string
}

func (s *Shark) SayHello() {
    fmt.Println("Hi! My name is", s.Name)
}

func main() {
    s := &Shark{"Sammy"}
    s = nil
    s.SayHello()
}

يكون الخرج كما يلي:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba]

goroutine 1 [running]:
main.(*Shark).SayHello(...)
    /tmp/sandbox160713813/prog.go:12
main.main()
    /tmp/sandbox160713813/prog.go:18 +0x1a

عرّفنا في هذا المثال سجل struct اسميناه Shark يحتوي على تابع وحيد مُعرَّف ضمن مُستقبل مؤشرها واسمه SayHello والذي يطبع تحية عند استدعائه، كما نُنشئ داخل جسم الدالة main نسخةً جديدةً من السجل Shark ونحاول أخذ مؤشر عليها باستخدام العامِل &، إذ يُسنَد المؤشر إلى المتغير s ثم نُسند المتغير s مرةً أخرى إلى القيمة nil من خلال التعليمة s = nil ثم نحاول استدعاء التابع SayHello على المتغيرs.

نتيجةً لذلك نحصل على حالة انهيار تُشير إلى محاولة الوصول إلى عنوان ذاكرة غير صالح لأن المتغير s كانت قيمته nil، وبالتالي عند استدعاء الدالة SayHello فإنه يحاول الوصول إلى الحقل Name الموجود على النوع ‎*Shark، وبالتالي تحدث حالة انهيار لأنه لا يمكن تحصيل dereference قيمة غير موجودة nil بما أنّ المؤشر هو مؤشر استقبال والمستقبل في هذه الحالة هو nil.

حددنا في هذا المثال القيمة nil صراحةً، لكن تظهر هذه القيم في الحالات العملية من دون انتباهنا، إذًا، عندما تظهر لديك حالات انهيار تتضمن الرسالة nil pointer dereference، ينبغي عليك التحقق مما إذا كنت تُسند قيمة nil إلى أحد المؤشرات التي تستخدِمها في برنامجك.

استخدام دالة panic المضمنة

حالات الانهيار الناتجة عن تجاوز حدود الشريحة أو تلك الناتجة عن المؤشرات ذات القيمة nil هي الأشهر، لكن من الممكن أيضًا أن نُظهر حالة انهيار يدويًا من خلال الدالة المُضمّنة panic التي تأخذ وسيط واحد فقط يُمثِّل السلسلة النصية التي ستمثّل رسالة الانهيار، وعادةً ما يكون عرض هذه الرسالة أكثر راحةً من تعديل الشيفرة بحيث تعيد خطأً، كما يمكننا أيضًا استخدامها داخل الحزم الخاصة بنا لتنبيه المطورين إلى أنهم قد ارتكبوا خطأً عند استخدام شيفرة الحزمة خاصتنا، عمومًا، يُفضَّل إعادة قيم الخطأ error إلى مُستخدمِي الحزمة، وسيطلق المثال التالي حالة انهيار ناتجةً عن دالة تُستدعى من دالة أخرى:

package main

func main() {
    foo()
}

func foo() {
    panic("oh no!")
}

ستكون حالة الانهيار الناتجة كما يلي:

panic: oh no!

goroutine 1 [running]:
main.foo(...)
    /tmp/sandbox494710869/prog.go:8
main.main()
    /tmp/sandbox494710869/prog.go:4 +0x40

عرّفنا في هذا المثال الدالة foo التي تستدعي الدالة panic والتي نُمرر لها السلسلة "oh no!‎" ثم استدعينا هذه الدالة foo من داخل الدالة main.

لاحظ أنّ الخرج يتضمن الرسالة التي حددناها للدالة panic ويعرض مسار المكدس لنا روتينًا واحدًا وسطرين من المسارات؛ الأول للدالة ()main والثاني للدالة ()foo.

وجدنا مما سبق أنّ حالات الانهيار تؤدي إلى إنهاء البرنامج لحظة حدوثها، ويمكن أن يؤدي ذلك إلى حدوث مشكلات عند وجود موارد مفتوحة يجب إغلاقها بطريقة سليمة، وهنا توفِّر جو آليةً لتنفيذ بعض التعليمات البرمجية دائمًا حتى عندما تظهر حالة الانهيار.

الدوال المؤجلة

قد يحتوي برنامجك على موارد يجب تنظيفها بطريقة سليمة، حتى أثناء معالجة حالات الانهيار في وقت التشغيل، إذ تسمح لك جو بتأجيل تنفيذ استدعاء دالة ريثما تكون الدالة المُستدعاة ضمنها قد أكتمل تنفيذها.

يُمكن للدوال المؤجلة أن تُنفّذ حتى عند ظهور حالة انهيار وتستخدم بوصفها تقنية أمان للحماية من الفوضى والمشكلات التي قد تسببها حالة الانهيار، ولتأجيل دالة نستخدِم الكلمة المفتاحية defer قبل اسم الدالة كما يلي ()defer sayHello، ولاحظ في المثال التالي أن الرسالة ستظهر رغم حدوث حالة انهيار:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("hello from the deferred function!")
    }()

    panic("oh no!")
}

يكون الخرج كما يلي:

hello from the deferred function!
panic: oh no!

goroutine 1 [running]:
main.main()
    /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

نؤجل استدعاء دالة مجهولة الاسم داخل الدالة ()main والتي تطبع الرسالة "hello from the deferred function!‎" ثم تُطلق الدالة ()main حالة انهيار من خلال استدعاء الدالة panic، ونلاحظ من الخرج أنّ الدالة المؤجلة تُنفّذ أولًا وتُطبع الرسالة الخاصة بها، ثم تظهر لنا رسالة حالة الانهيار.

توفر جو لنا أيضًا فرصة منع حالة الانهيار من إنهاء البرنامج من داخل الدوال المؤجلة من خلال دالة مُضمّنة أخرى.

معالجة حالات الانهيار

تمتلك جو تقنية معالجة واحدة تتجسّد في الدالة recover، إذ تسمح لك هذه الدالة في اعتراض حالة الانهيار من مسار المكدس ومنع الإنهاء المُفاجئ للبرنامج.

هناك قواعد صارمة عند التعامل معها، لكنها دالة لا تقُدّر بثمن في تطبيقات الإنتاج، ويمكن استدعاء الدالة recover مباشرةً دون الحاجة إلى استيراد أيّ حزمة لأنها دالة مُضمّنة كما ذكرنا.

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    return a / b
}

يكون الخرج كما يلي:

2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!

تُستدعى الدالة divideByZero من داخل الدالة main، كما داخل هذه الدالة نؤجل استدعاء دالة مجهولة الاسم ومسؤولة عن التعامل مع أيّ حالات انهيار قد تنشأ أثناء تنفيذ divideByZero، ونستدعي داخل هذه الدالة مجهولة الاسم الدالة المُضمّنة recover ونسند الخطأ الذي تُعيده إلى المتغير err.

إذا ظهرت حالة انهيار في الدالة ()divideByZero، فستُهيّأ قيمة هذا الخطأ، وإلا فستكون nil، ومن خلال مقارنة قيمة المتغير err بالقيمة nil، سنستطيع تحديد فيما إذا كانت حالة الانهيار قد حدثت، وفي هذه الحالة نسجّل حالة الانهيار من خلال الدالة log.Println كما لو كانت أيّ خطأ آخر.

نلاحظ أننا نستدعي الدالة divide ونحاول طباعة ناتجها باستخدام الدالة ()fmt.Println بتتبع الدالة المؤجلة مجهولة الاسم بما أن وسيط هذه الدالة سيُسبب القسمة على صفر، وبالتالي ستظهر حالة انهيار.

يُظهر خرج البرنامج رسالة الدالة ()log.println من الدالة مجهولة الاسم والتي تُعالج حالة الانهيار متبوعة برسالة we survived dividing by zero!‎، والتي تُشير إلى أننا منعنا حالة الانهيار من إيقاف البرنامج، وذلك بفضل استخدام دالة المعالجة recover.

قيمة الخطأ err المُعادة من الدالة ()recover هي نفسها القيمة المُعطاة لاستدعاء الدالة ()panic، لذا من المهم التأكد مما إذا كانت قيمتها nil.

اكتشاف حالات الانهيار باستخدام recover

تعتمد الدالة recover على قيمة الخطأ في اكتشاف حدوث حالات الانهيار، وبما أن وسيط الدالة panic هو واجهة فارغة، لذا يمكن أن تكون من أي نوع، كما أنّ القيمة الصفرية لأي نوع بيانات يُمثّل واجهةً هو nil، ويوضِّح المثال التالي أنه يجب تجنب تمرير القيمة nil على أساس وسيط للدالة panic:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    if b == 0 {
        panic(nil)
    }
    return a / b
}

يكون الخرج كما يلي:

we survived dividing by zero!

هذا المثال هو المثال السابق نفسه مُتضمنًا الدالة recover مع بعض التعديلات الطفيفة، إذ عدّلنا دالة القسمة للتحقق مما إذا كان المقسوم عليه b يساوي 0، فإذا كان كذلك، فسيولّد حالة انهيار باستخدام الدالة panic مع وسيط من القيمة nil.

لن يتضمن الخرج في هذه المرة رسالة الدالة log التي تعرض حدوث حالة الانهيار بالرغم من حدوث حالة انهيار نتيجة القسمة، ويؤكد هذا السلوك الصامت أهمية التحقق من قيمة الخطأ فيما إذا كانت nil.

الخاتمة

رأينا العديد من الطرق التي تؤدي إلى ظهور حالات انهيار في البرنامج، كما أننا عرضنا طرقًا لإنشائها يدويًا، ثم راجعنا الدالة المضمنة recover التي تُمكّننا من معالجة حالات الانهيار، وليس من الضروري أن تحتاج إلى التعامل مع حالات الانهيار في برامجك، لكن عندما يتعلق الأمر بتطبيقات الإنتاج فهي بغاية الأهمية.

ترجمة -وبتصرُّف- للمقال Handling Panics inGo لصاحبه Gopher Guides.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...