أخطاء Sentinel
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 55 من 107.
يُعد خطأ الحارس (sentinel error) متغير خطأ معرفاً مسبقاً على مستوى الحزمة، ويمثل حالة خطأ محددة. وعلى عكس إنشاء أخطاء جديدة في كل مرة باستخدام errors.New، يتم التصريح عن أخطاء الحارس مرة واحدة وإعادة استخدامها في جميع أنحاء الكود الخاص بك.
تقوم بتعريف أخطاء الحارس (sentinel errors) كمتغيرات على مستوى الحزمة، وعادةً ما تبدأ أسماؤها بـ Err:
package user
import "errors"
var (
ErrNotFound = errors.New("user not found")
ErrInvalidEmail = errors.New("invalid email address")
ErrDuplicate = errors.New("user already exists")
)تُرجع الدوال هذه الأخطاء المحددة مسبقاً عند حدوث شروط معينة:
func FindUser(id int) (*User, error) {
user, exists := users[id]
if !exists {
return nil, ErrNotFound
}
return user, nil
}تكمن قوة أخطاء الحارس (sentinel errors) في قدرة المستدعين على التحقق من حالات خطأ معينة عبر مقارنة قيم الأخطاء مباشرة:
user, err := FindUser(42)
if err == ErrNotFound {
fmt.Println("Creating new user...")
} else if err != nil {
fmt.Println("Unexpected error:", err)
}تستخدم مكتبة Go القياسية أخطاء الحارس (sentinel errors) بشكل مكثف. على سبيل المثال، يشير io.EOF إلى نهاية الإدخال، ويشير sql.ErrNoRows إلى نتيجة استعلام فارغة. ينشئ هذا النمط عقداً واضحاً بين الحزم ومستخدميها حول الأخطاء المتوقع حدوثها.
ومع ذلك، فإن عمليات التحقق البسيطة من المساواة تتعطل عندما يتم تغليف الأخطاء. في الدرس القادم، ستتعلم كيف يقوم errors.Is() بحل هذه المشكلة عن طريق التحقق من سلسلة الأخطاء بالكامل.
التحدي
سهللنقم ببناء نظام لإدارة المخزون يستخدم أخطاء الحارس (sentinel errors) للإبلاغ عن حالات فشل محددة. ستقوم بتعريف متغيرات خطأ على مستوى الحزمة (package-level) بحيث يمكن للمستدعين التحقق منها للتعامل مع السيناريوهات المختلفة بشكل مناسب.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
inventory.go: أنشئ نظام مخزون مع أخطاء حارس محددة مسبقاً لحالات الفشل الشائعة.قم بتعريف ثلاثة أخطاء حارس على مستوى الحزمة:
ErrItemNotFoundمع الرسالة"item not found in inventory"ErrOutOfStockمع الرسالة"item is out of stock"ErrInsufficientQuantityمع الرسالة"insufficient quantity available"
أنشئ هيكل
Inventoryيحتوي على خريطة (map) لأسماء العناصر وكمياتها. قم بتنفيذ هذه الدوال (methods):NewInventory() *Inventory- ينشئ مخزوناً فارغاًAddItem(name string, quantity int)- يضيف أو يحدث كمية عنصر ماGetQuantity(name string) (int, error)- يعيد كمية العنصر، أوErrItemNotFoundإذا لم يكن موجوداًPurchase(name string, quantity int) error- يحاول شراء عناصر. أعدErrItemNotFoundإذا لم يكن العنصر موجوداً، أوErrOutOfStockإذا كانت الكمية الحالية صفراً، أوErrInsufficientQuantityإذا كانت الكمية المطلوبة تتجاوز المخزون المتاح. في حال النجاح، قم بتقليل المخزون وأعدnil.
main.go: اقرأ عمليات المخزون من المدخلات ووضح كيف يمكن للمستدعين التحقق من أخطاء حارس محددة باستخدام مقارنة المساواة المباشرة.اقرأ اسم العنصر وكمية الشراء من المدخلات. أنشئ مخزوناً، وأضف العنصر
"laptop"بكمية5، ثم حاول إجراء عملية الشراء. بناءً على الخطأ المعاد، اطبع رسالة محددة توضح أنك حددت أي خطأ حارس قد حدث.
سيتم توفير المدخلات التالية:
- السطر 1: اسم العنصر المراد شراؤه
- السطر 2: الكمية المراد شراؤها
تعامل مع النتيجة عن طريق التحقق من الخطأ المحدد الذي تمت إعادته:
- إذا كان
err == ErrItemNotFound: اطبعError: Item '[name]' does not exist in our inventory - إذا كان
err == ErrOutOfStock: اطبعError: Item '[name]' is currently out of stock - إذا كان
err == ErrInsufficientQuantity: اطبعError: Cannot purchase [quantity] units of '[name]' - not enough in stock - في حال النجاح: اطبع
Successfully purchased [quantity] units of '[name]'
على سبيل المثال، إذا تم إدخال laptop و 3، يجب أن يكون مخرجك:
Successfully purchased 3 units of 'laptop'وإذا تم إدخال phone و 2، يجب أن يكون مخرجك:
Error: Item 'phone' does not exist in our inventoryوإذا تم إدخال laptop و 10، يجب أن يكون مخرجك:
Error: Cannot purchase 10 units of 'laptop' - not enough in stockورقة مرجعية
يُعد الخطأ المرجعي (sentinel error) متغيراً للخطأ محدد مسبقاً على مستوى الحزمة، ويمثل حالة خطأ محددة. يتم التصريح عن الأخطاء المرجعية مرة واحدة ويُعاد استخدامها في جميع أنحاء الكود الخاص بك.
قم بتعريف الأخطاء المرجعية كمتغيرات على مستوى الحزمة، وعادةً ما تبدأ أسماؤها بـ Err:
package user
import "errors"
var (
ErrNotFound = errors.New("user not found")
ErrInvalidEmail = errors.New("invalid email address")
ErrDuplicate = errors.New("user already exists")
)تُرجع الدوال هذه الأخطاء المحددة مسبقاً عند حدوث حالات معينة:
func FindUser(id int) (*User, error) {
user, exists := users[id]
if !exists {
return nil, ErrNotFound
}
return user, nil
}يمكن للمستدعين التحقق من حالات خطأ محددة عن طريق مقارنة قيم الأخطاء مباشرة:
user, err := FindUser(42)
if err == ErrNotFound {
fmt.Println("Creating new user...")
} else if err != nil {
fmt.Println("Unexpected error:", err)
}تستخدم مكتبة Go القياسية الأخطاء المرجعية بشكل مكثف، مثل io.EOF و sql.ErrNoRows.
جرّب بنفسك
package main
import (
"fmt"
)
func main() {
// قراءة المدخلات
var itemName string
var quantity int
fmt.Scanln(&itemName)
fmt.Scanln(&quantity)
// إنشاء المخزون وإضافة "laptop" بكمية 5
inv := NewInventory()
inv.AddItem("laptop", 5)
// TODO: محاولة شراء العنصر
// استدعاء inv.Purchase(itemName, quantity)
// TODO: التحقق من خطأ sentinel المحدد الذي تم إرجاعه باستخدام مقارنة المساواة المباشرة
// استخدم عبارات if/else if للتحقق من:
// - err == ErrItemNotFound
// - err == ErrOutOfStock
// - err == ErrInsufficientQuantity
// - err == nil (نجاح)
// TODO: طباعة الرسالة المناسبة بناءً على الخطأ
// في حال ErrItemNotFound: fmt.Printf("Error: Item '%s' does not exist in our inventory\n", itemName)
// في حال ErrOutOfStock: fmt.Printf("Error: Item '%s' is currently out of stock\n", itemName)
// في حال ErrInsufficientQuantity: fmt.Printf("Error: Cannot purchase %d units of '%s' - not enough in stock\n", quantity, itemName)
// في حال النجاح: fmt.Printf("Successfully purchased %d units of '%s'\n", quantity, itemName)
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP في Go
الملفات الخارجيةمساحة العمل والـ Modules في Goالـ Packages والـ Importsالأسماء المصدرة مقابل غير المصدرةمقدمة إلى الـ OOP في Goالـ Structs كـ Classesتعريف الـ Methods في الـ Structsالـ Pointer Receivers مقابل الـ Value Receiversتهيئة الـ Structدوال الـ Constructorمراجعة - آلة حاسبة بسيطة4الواجهات
مقدمة في الواجهاتالتنفيذ الضمنيالواجهة كعقدالواجهة الفارغة (any)تأكيد النوع (Type Assertion)تبديل النوع (Type Switch)تركيب الواجهاتواجهات Stringer و Errorمراجعة - حاسبة الأشكال7التغليف
الحقول المصدرة وغير المصدرةالتغليف على مستوى الحزمةدوال الـ Getter والـ Setterإخفاء المعلومات في Goملخص - سجلات الطلاب10الأنواع العامة (Go 1.18+)
مقدمة في الأنواع العامةمعاملات الأنواعقيود الأنواعالـ Structs العامةحل بديل للـ Methods العامةملخص - المجموعات العامة13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factoryنمط Abstract Factoryنمط Observerنمط Strategy2تعمق في الأنواع و Structs
الأنواع الأساسية والمركبةتعريفات الأنواع المخصصةStruct TagsStructs مجهولةStructs متداخلةالقيم الصفرية والافتراضيةمراجعة - دفتر العناوين5التركيب بدلاً من الوراثة
لماذا لا تدعم Go الوراثةأساسيات تضمين الـ Structترقية الـ Methodتضمين عدة Structsالتضمين مقابل التجميعحجب الـ Methods المضمنةملخص - الهيكل الهرمي للموظفين8معالجة الأخطاء و OOP
واجهة errorأنواع الأخطاء المخصصةتغليف الأخطاء (fmt.Errorf)أخطاء Sentinelerrors.Is() و errors.As()Panic و Defer و Recoverملخص - File Parser11المكتبة القياسية والبرمجة كائنية التوجه (OOP)
io.Reader و io.Writerواجهة sort.Interfaceواجهة fmt.Stringerencoding/json مع Structsواجهة http.Handlerمراجعة - نماذج REST API14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبMiddleware كنمط مزين3المؤشرات والذاكرة
أساسيات المؤشرات في Goالمؤشرات إلى الـ Structsالتمرير بالقيمة مقابل التمرير بالمرجعدالة ()newGarbage Collection في Goملخص - بناء Linked List6تعدد الأشكال في Go
تعدد الأشكال عبر InterfacesDuck Typing في Goقواعد استيفاء الـ Interfaceمجموعات متعددة الأشكالحقن التبعيةمراجعة - معالج الدفع9التزامن والبرمجة كائنية التوجه (OOP)
أساسيات الـ Goroutinesالقنوات (Channels) والاتصالالقنوات المخزنة (Buffered) مقابل غير المخزنةجملة Selectsync.Mutex و sync.RWMutexsync.WaitGroupتصميم الـ Structs الآمنة للخيوط (Thread-Safe)مراجعة - Worker Pool