معالجة الأخطاء في Go: try/catch وما بعدها — المستوى المتقدم
\n\nعلى عكس العديد من اللغات الحديثة (Java، Python، JavaScript)، لا تحتوي Go على بنيات تقليدية مثل try/catch/finally. غالبًا ما ينظر المطورون القادمون من أنظمة بيئية أخرى إلى هذا على أنه عيب. ومع ذلك، تقدم فلسفة Go طريقة أكثر وضوحًا وقابلية للتنبؤ للتعامل مع الأخطاء. في هذه المقالة، سنستعرض التقنيات المتقدمة لمعالجة الأخطاء في Go: من المبادئ الأساسية إلى إنشاء أنظمة مرنة باستخدام panic/recover، وتغليف الأخطاء، والأنواع المخصصة.
1. لماذا لا يوجد try/catch في Go؟
\n\nالسبب الرئيسي هو البساطة وقابلية قراءة الكود. في Go، الخطأ هو مجرد قيمة (error) تعيدها الدالة كنتيجة أخيرة. يتحقق المطور بشكل صريح من هذه القيمة، بدلاً من الاعتماد على تدفق استثناءات مخفي. وهذا يجعل تدفق تنفيذ البرنامج واضحًا.
النمط القياسي يبدو كالتالي:
\n\npackage main\n\nimport (\n "fmt"\n "os"\n)\n\nfunc readFile(name string) ([]byte, error) {\n data, err := os.ReadFile(name)\n if err != nil {\n return nil, fmt.Errorf("تعذر قراءة الملف %s: %w", name, err)\n }\n return data, nil\n}\n\nfunc main() {\n data, err := readFile("config.json")\n if err != nil {\n fmt.Println("خطأ:", err)\n return\n }\n fmt.Println("البيانات:", string(data))\n}\n\nهنا، يتم معالجة كل خطأ محتمل بشكل صريح. هذا ليس "ضوضاء"، بل ضمان بأن المبرمج قد فكر في الفشل المحتمل.
\n\n2. التقنيات المتقدمة: التغليف وسلاسل الأخطاء
\n\nقدم Go 1.13 دعم تغليف الأخطاء من خلال %w في fmt.Errorf. يتيح ذلك إنشاء سلاسل من الأخطاء مع الحفاظ على السبب الأصلي. لفك هذه السلاسل، تُستخدم الدالتان errors.Is و errors.As.
مثال على التغليف والتحقق:
\n\npackage main\n\nimport (\n "errors"\n "fmt"\n)\n\nvar ErrNotFound = errors.New("السجل غير موجود")\n\nfunc findUser(id int) (string, error) {\n if id != 42 {\n return "", fmt.Errorf("البحث عن المستخدم %d: %w", id, ErrNotFound)\n }\n return "Alice", nil\n}\n\nfunc main() {\n _, err := findUser(100)\n if errors.Is(err, ErrNotFound) {\n fmt.Println("خطأ: السجل غير موجود. التفاصيل:", err)\n } else {\n fmt.Println("خطأ غير معروف:", err)\n }\n}\n\nاستخدام errors.As للأخطاء المكتوبة:
package main\n\nimport (\n "errors"\n "fmt"\n)\n\ntype ValidationError struct {\n Field string\n Msg string\n}\n\nfunc (e *ValidationError) Error() string {\n return fmt.Sprintf("خطأ في التحقق من صحة الحقل %s: %s", e.Field, e.Msg)\n}\n\nfunc validateAge(age int) error {\n if age < 0 {\n return &ValidationError{Field: "age", Msg: "العمر لا يمكن أن يكون سالبًا"}\n }\n return nil\n}\n\nfunc main() {\n err := validateAge(-5)\n var ve *ValidationError\n if errors.As(err, &ve) {\n fmt.Printf("الحقل: %s، الرسالة: %s\\n", ve.Field, ve.Msg)\n } else {\n fmt.Println("خطأ غير معروف:", err)\n }\n}\n\n3. Panic و Recover: الاسترداد من الطوارئ
\n\nعلى الرغم من أن panic في Go هو نظير للاستثناء، إلا أنه يجب استخدامه فقط في حالات استثنائية حقًا (مثل خطأ منطقي من المبرمج، القسمة على صفر، تجاوز حدود المصفوفة). يسمح recover باعتراض الذعر واستعادة التنفيذ.
هام: يعمل recover فقط داخل دالة مؤجلة (defer). السيناريو النموذجي هو middleware في خادم HTTP أو حماية goroutine.
مثال على تشغيل goroutine محمي:
\n\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc safeGoroutine(fn func()) {\n go func() {\n