Menu
flag Ar iconالعربيةdown icon

معالجة الأخطاء في Golang: if err != nil والتغليف والفحص

تعامل Go الأخطاء كقيم عادية تعيدها الدوال. تعلّم الواجهة error، وif err != nil، وerrors.New وfmt.Errorf، وإعادة الأخطاء مع سياق، وفحصها بـ errors.Is وerrors.As، ومعالجة كل خطأ مرة واحدة.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

الأخطاء قيم

دالة Go التي قد تفشل تعيد error كآخر نتيجة. ويفحصها المستدعي فورًا.

المخرجات:

parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax

هذه هي الآلية كلها. لا استثناءات، ولا try أو catch، ولا تدفّق تحكّم خفي: الخطأ ينتقل فقط حيث تمرّره شيفرتك. الثمن هو تكرار ظاهر لـ if err != nil. والفائدة أن كل نقطة فشل ظاهرة في الصفحة، وأنت تقرّر عند كل منها ما يحدث.

النوع error

error واجهة مدمجة لها تابع واحد:

type error interface {
	Error() string
}

أي نوع له تابع Error() string هو خطأ. الخطأ nil يعني النجاح. طباعة الخطأ بـ fmt.Println(err) أو %v تستدعي Error().

إنشاء الأخطاء

دالتان تغطّيان معظم الحالات.

تنشئ errors.New خطأ بنص ثابت. وتنسّق fmt.Errorf خطأ بالأفعال نفسها التي تستخدمها Printf. اصطلاحًا تبدأ نصوص الأخطاء بحرف صغير ولا تنتهي بعلامة ترقيم، لأنها تُضمَّن عادة في رسائل أطول: load config: open app.yaml: no such file or directory.

نمط if err != nil

الشكل المعتاد: استدعِ، افحص، عُد مبكرًا. يبقى مسار النجاح عند بداية السطر، ويخرج كل فشل لحظة حدوثه.

func loadUser(id int) (*User, error) {
	row, err := db.Query(id)
	if err != nil {
		return nil, err
	}
	u, err := parseUser(row)
	if err != nil {
		return nil, err
	}
	if err := u.Validate(); err != nil {
		return nil, err
	}
	return u, nil
}

عرفان تجدر ملاحظتهما:

  • عند الخطأ أعد القيمة الصفرية للنتائج الأخرى (nil و0 و""). ويجب ألا يستخدمها المستدعون عندما يكون err != nil.
  • تحصر if err := f(); err != nil نطاق err داخل if عندما تعيد الدالة خطأ فقط. وهذا يبقي النطاق الخارجي نظيفًا.

تجنّب else بعد إعادة خطأ. if err != nil { return err } else { ... } يزيح المسار الناجح بلا سبب.

إضافة سياق عند إعادة الخطأ

الخطأ الذي يُمرَّر صعودًا دون تغيير يفقد قصة مصدره. open config.yaml: no such file or directory لا تخبرك أي خطوة من بدء التشغيل فشلت. أضف سياقًا بـ fmt.Errorf والفعل %w:

المخرجات:

start server: read config: open /etc/myapp/config.yaml: no such file or directory
true

كل طبقة تضيف ما كانت تفعله، وتُقرأ الرسالة النهائية كأثر من أعلى الاستدعاء حتى السبب. السياق الجيد يسمّي العملية والمدخل: parse line 12 وfetch user 42. لا تضف "error" أو "failed" في كل مستوى؛ الرسالة خطأ أصلًا.

%w يغلّف: يحتفظ بالخطأ الأصلي داخل الجديد، فتستطيع errors.Is وerrors.As إيجاده. أما %v فينسخ النص فقط. استخدم %v عندما تريد عمدًا إخفاء تفصيل داخلي عن المستدعين، مثلًا كي لا يعتمدوا على نوع خطأ مشغّل قاعدة بيانات.

فحص أخطاء محدّدة: errors.Is وerrors.As

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

قواعد عملية:

  • قارن بقيم الأخطاء المعرّفة مسبقًا (الأخطاء الحارسة مثل io.EOF وos.ErrNotExist وsql.ErrNoRows) بـ errors.Is لا بـ ==. تفشل == بمجرّد تغليف الخطأ.
  • استخرج الخطأ ذا النوع بـ errors.As لا بتأكيد نوع، للسبب نفسه. تأخذ errors.As مؤشرًا إلى متغيّر من النوع الهدف.
  • لا تطابق على نص err.Error() أبدًا. الرسائل تتغيّر بين الإصدارات، والمطابقة النصية تنكسر بصمت عندما يحدث ذلك.

تعريف أخطائك الحارسة وأنواع أخطائك الخاصة، وجمع عدة أخطاء بـ errors.Join، مشروحة في صفحة الأخطاء المخصّصة.

عالج الخطأ مرة واحدة

يجب أن يُعالج الخطأ مرة واحدة بالضبط. والمعالجة تعني أحد الأمور التالية: إعادته (مغلّفًا عادة)، أو تسجيله والمتابعة، أو إعادة المحاولة، أو تحويله إلى ردّ للمستخدم. فعل اثنين منها هو أشيع خطأ في معالجة الأخطاء في شيفرات Go.

// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
	log.Printf("could not fetch user: %v", err)
	return err
}

// Right: add context and return. The top of the program logs once.
if err != nil {
	return fmt.Errorf("fetch user %d: %w", id, err)
}

التسجيل مع الإعادة ينتج الفشل نفسه عدة مرات في السجلات، كل مرة بسياق أقل من الرسالة النهائية. دع الأخطاء تصعد إلى المكان القادر على تقرير ما يُفعل (معالج HTTP، أو main، أو حلقة عامل)، وسجّلها هناك.

أين تنتهي الأخطاء

في أعلى البرنامج يجب أن يتصرّف شيء ما حيال الخطأ. في main يعني ذلك عادة طباعته والخروج بحالة غير صفرية:

تشغيل هذا دون وسائط يطبع error: usage: app <name> إلى stderr ويخرج بالحالة 1 (اكتب اسمًا في لوحة Args لترى المسار الآخر). إبقاء main بهذا الشكل، والعمل الحقيقي في run، يجعل البرنامج قابلًا للاختبار ويعني أن عبارات defer داخل run ما زالت تُنفَّذ، لأن os.Exit تتخطّى الاستدعاءات المؤجّلة.

في خادم HTTP يكون الأعلى هو المعالج: يحوّل الخطأ إلى رمز حالة ورسالة آمنة للعميل، ويسجّل الرسالة المفصّلة لك.

أخطاء قد تتجاهلها، وأخرى لا يجوز

تجاهل الخطأ صحيح أحيانًا، لكن اجعله صريحًا بـ _ ليعرف القرّاء أنه كان قرارًا:

_ = conn.SetDeadline(t) // best effort

بعض الاستدعاءات لا تفشل عمليًا (strings.Builder.WriteString وbytes.Buffer.Write). وأخرى تبدو غير مؤذية وهي ليست كذلك: Close على ملف كتبت فيه قد تبلغ أن البيانات لم تصل إلى القرص قط، وjson.Marshal تفشل مع القنوات والدوال. عند الشك افحص.

تبلغ أداة الفحص errcheck (المضمّنة في golangci-lint) عن الأخطاء غير المفحوصة. أما go vet فلا يبلغ عنها وحده.

الأخطاء وpanic

لدى Go أيضًا panic، لكنها ليست نظام استثناءات. استخدم الأخطاء لكل ما قد يسوء في التشغيل العادي: مدخلات خاطئة، ملفات مفقودة، أعطال شبكة. واحتفظ بـ panic لأخطاء البرمجة (حالة مستحيلة، ثابت مكسور) ولإخفاقات بدء التشغيل التي لا معنى للمتابعة بعدها. يجب ألا تسبّب المكتبة panic عبر واجهتها البرمجية تقريبًا أبدًا. راجع صفحة panic وrecover.

تقليل التكرار

if err != nil مطوّلة، ورُفضت مقترحات إضافة صيغة جديدة لها مرارًا؛ وأعلن فريق Go في 2025 أنه لم يعد يسعى إلى تغييرات في صيغة معالجة الأخطاء. بعض الأنماط تقلّل الضجيج ضمن اللغة:

  • عُد مبكرًا وأبقِ الدوال صغيرة. معظم التكرار يأتي من دوال طويلة تنفّذ خطوات كثيرة.
  • الخطأ اللاصق. لسلسلة كتابات احتفظ بأول خطأ في حقل بنية واجعل الاستدعاءات اللاحقة لا تفعل شيئًا بعد ضبطه. تعمل bufio.Writer بهذه الطريقة: تفحص الخطأ مرة واحدة بعد Flush.
  • غلّف مرة واحدة لكل دالة. إغلاق مؤجّل على نتيجة مسمّاة يمكنه إضافة السياق نفسه إلى كل خطأ تعيده الدالة (راجع defer).

أخطاء شائعة

  • استخدام قيمة عندما يكون err غير nil. افحص أولًا، ثم استخدم.
  • التسجيل مع الإعادة. اختر واحدًا.
  • المقارنة بـ == بعد التغليف. استخدم errors.Is.
  • فقدان السبب بـ %v. استخدم %w ما لم يكن الإخفاء هو المقصود.
  • إعادة مؤشر nil ذي نوع كـ error. var e *MyErr; return e ليست nil عند المستدعي. أعد nil حرفيًا.
  • رسائل تبدأ بحرف كبير أو تنتهي بعلامة ترقيم. errors.New("Failed to connect.") تُقرأ بشكل سيئ بعد التغليف. اكتب connect to db: ....

الأسئلة الشائعة

كيف تعمل معالجة الأخطاء في Go؟

الدوال التي قد تفشل تعيد error كآخر نتيجة. يفحصها المستدعي فورًا: v, err := f(); if err != nil { return err }. الـ error قيمة واجهة عادية لها تابع واحد، Error() string، وnil تعني النجاح. لا توجد استثناءات.

هل تملك Go صيغة try/catch؟

لا. لا تملك Go استثناءات ولا try/catch. الإخفاقات المتوقّعة تُعاد كقيم error وتُفحص بـ if err != nil. توجد panic وrecover، لكنهما لأخطاء البرمجة والحالات التي لا يمكن التعافي منها، لا لتدفّق الأخطاء العادي.

كيف أعيد خطأ في Go؟

عرّف error كآخر نتيجة وأعد nil عند النجاح. أنشئ الأخطاء بـ errors.New("message") لنص ثابت أو fmt.Errorf("reading %s: %w", name, err) لإضافة سياق إلى خطأ تلقّيته. عند الفشل أعد القيم الصفرية للنتائج الأخرى.

ما الفرق بين %w و%v في fmt.Errorf؟

كلاهما يضع رسالة الخطأ الأصلي في الخطأ الجديد. لكن %w يغلّفه أيضًا، فتستطيع errors.Is وerrors.As إيجاد الأصل. أما %v فينتج خطأ جديدًا فيه النص فقط. استخدم %w عندما قد يحتاج المستدعون إلى فحص السبب، و%v عندما تريد إخفاءه.

كيف أعرف أي خطأ أُعيد في Go؟

استخدم errors.Is(err, target) للمقارنة بخطأ حارس مثل io.EOF أو os.ErrNotExist، وerrors.As(err, &target) لاستخراج نوع خطأ محدّد مثل *fs.PathError. كلاهما يمرّ عبر الأخطاء المغلّفة. تجنّب مقارنة نصوص err.Error().

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن