معالجة الأخطاء
الدرس 10: معالجة الأخطاء
تشبيه من الواقع
تخيل أنك تذهب إلى مطعم لطلب طعام. يخبرك النادل: "عذرًا، هذا الطبق نفد اليوم." — هذه متوقعة؛ يمكنك طلب شيء آخر، وتستمر الحياة. هذا هو error في Go.
لكن إذا اشتعلت المطبخ فجأة ويجب على الجميع الإخلاء فورًا — هذه كارثة غير متوقعة. هذا هو panic في Go.
فلسفة معالجة الأخطاء في Go مباشرة: تعامل مع المشاكل المتوقعة بأناقة، ولا تنهار إلا لغير المتوقعة. على عكس اللغات الأخرى التي تمزج جميع الاستثناءات بـ try/catch، تتطلب Go منك معالجة كل خطأ بشكل صريح وفردي.
المفاهيم الأساسية
| المفهوم | الوصف |
|---|---|
واجهة error |
نوع الخطأ المدمج في Go بأسلوب واحد فقط: Error() string |
| قيم الإرجاع المتعددة | دوال Go عادةً تُرجع الأخطاء كقيمة إرجاع أخيرة |
errors.New |
يُنشئ خطأ نصي بسيط |
fmt.Errorf |
يُنشئ خطأ منسقًا (يمكن تغليفه بـ %w) |
errors.Is |
يتحقق مما إذا كانت سلسلة الأخطاء تحتوي على خطأ محدد |
errors.As |
يستخرج نوع خطأ محدد من سلسلة الأخطاء |
panic |
يُطلق panic وقت التشغيل، مُتسببًا في انهيار البرنامج |
recover |
يلتقط panic في prevent لمنع انهيار البرنامج |
| تغليف الأخطاء | تغليف الأخطاء الأساسية بـ %w أو أنواع مخصصة للحفاظ على السياق |
مخطط تدفق معالجة الأخطاء
الدالة تُرجع خطأ
│
▼
err != nil ?
┌────┴────┐
│ نعم │ لا
▼ ▼
معالجة الخطأ المتابعة
│
├─ قابل للتعافي → سجل / إرجاع قيمة افتراضية / إعادة المحاولة
├─ يحتاج للإبلاغ → تغليف وتمرير للأعلى
└─ غير قابل للتعافي → panic
الصيغة الأساسية والاستخدام
1. واجهة error
error هو نوع الواجهة المدمج في Go، مُعرَّف بإيجاز شديد:
// تعريف واجهة error (مدمج، لا حاجة لاستيراد)
type error interface {
Error() string
}
أي نوع يُنفذ أسلوب Error() string هو error.
2. إنشاء الأخطاء
package main
import (
"errors"
"fmt"
)
func main() {
// الطريقة 1: errors.new — إنشاء خطأ نصي بسيط
err1 := errors.New("file not found")
// الطريقة 2: fmt.Errorf — إنشاء خطأ منسق
filename := "config.yaml"
err2 := fmt.Errorf("failed to read file %s", filename)
// الطريقة 3: fmt.Errorf + %w — تغليف الخطأ الأساسي (موصى به)
baseErr := errors.New("permission denied")
err3 := fmt.Errorf("cannot write log: %w", baseErr)
fmt.Println(err1) // file not found
fmt.Println(err2) // failed to read file config.yaml
fmt.Println(err3) // cannot write log: permission denied
}
3. التحقق من الأخطاء
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("record not found")
func findUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "Alice", nil
}
func main() {
name, err := findUser(0)
if err != nil {
// errors.Is يتحقق مما إذا كانت سلسلة الأخطاء تحتوي على الخطأ المستهدف
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found, please check the ID")
} else {
fmt.Println("Unknown error:", err)
}
return
}
fmt.Println("Found user:", name)
}
4. استخراج أنواع أخطاء محددة
package main
import (
"errors"
"fmt"
)
// نوع خطأ مخصص
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed [%s]: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "age must be between 0 and 150",
}
}
return nil
}
func main() {
err := validateAge(200)
if err != nil {
// errors.As يستخرج نوعًا محددًا من سلسلة الأخطاء
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("Field: %s, Reason: %s\n", ve.Field, ve.Message)
} else {
fmt.Println("Other error:", err)
}
}
}
5. panic و recover
package main
import "fmt"
// safeDivide يستخدم recover لالتقاط panic
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("caught panic: %v", r)
}
}()
// القسمة على صفر تُطلق panic
return a / b, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Error: caught panic: runtime error: integer divide by zero
return
}
fmt.Println("Result:", result)
}
error، وليس panic. استخدم panic فقط عندما لا يستطيع البرنامج الاستمرار حقًا (مثل فشل التهيئة، أخطاء منطقية غير قابلة للتعافي).
fmt.Errorf، استخدم دائمًا %w بدل %v. %w يُبقي الخطأ الأصلي، مما يتيح لـ errors.Is و errors.As العمل بشكل صحيح.
الأمثلة
مثال: معالجة أساسية للأخطاء (الصعوبة ⭐)
محاكاة قارئ ملف إعدادات بسيط لإظهار نمط إرجاع والتحقق من الخطأ الأكثر أساسية.
package main
import (
"errors"
"fmt"
"os"
)
// تعريف أخطاء رقيب على مستوى الحزمة
var (
ErrFileNotFound = errors.New("config file not found")
ErrFileEmpty = errors.New("config file is empty")
ErrInvalidFormat = errors.New("invalid config file format")
)
// Config يمثل إعدادات التطبيق
type Config struct {
Host string
Port int
}
// LoadConfig يُحمّل الإعدادات من ملف (محاكاة)
func LoadConfig(path string) (*Config, error) {
// محاكاة عدم العثور على الملف
if path == "" {
return nil, ErrFileNotFound
}
// محاكاة قراءة الملف
data, err := os.ReadFile(path)
if err != nil {
// تغليف الخطأ الأساسي، الحفاظ على المعلومات الأصلية
return nil, fmt.Errorf("reading config file %s: %w", path, err)
}
// التحقق مما إذا كان الملف فارغًا
if len(data) == 0 {
return nil, ErrFileEmpty
}
// محاكاة تحليل الإعدادات
return &Config{
Host: "localhost",
Port: 8080,
}, nil
}
func main() {
// محاولة تحميل الإعدادات
config, err := LoadConfig("app.conf")
if err != nil {
// استخدام errors.Is لتحديد نوع الخطأ
switch {
case errors.Is(err, ErrFileNotFound):
fmt.Println("Error: Please specify the config file path")
case errors.Is(err, ErrFileEmpty):
fmt.Println("Error: Config file is empty, please check contents")
case errors.Is(err, ErrInvalidFormat):
fmt.Println("Error: Config file format is invalid")
default:
fmt.Println("Error:", err)
}
return
}
fmt.Printf("Config loaded successfully: %s:%d\n", config.Host, config.Port)
}
الناتج (عندما لا يوجد الملف):
Error: Please specify the config file path
مثال: تغليف الأخطاء وفحص السلسلة (الصعوبة ⭐⭐)
يُظهر تغليف الأخطاء وانتشارها وفحص السلسلة في استدعاءات متعددة الطبقات.
package main
import (
"errors"
"fmt"
)
// تعريف أخطاء الأعمال
var (
ErrUserNotFound = errors.New("user not found")
ErrPermission = errors.New("insufficient permissions")
)
// يمثل أخطاء طبقة قاعدة البيانات
type DatabaseError struct {
Operation string
Table string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error [%s.%s]: %e", e.Table, e.Operation, e.Err)
}
func (e *DatabaseError) Unwrap() error {
return e.Err
}
// UserRepository مستودع المستخدمين
type UserRepository struct {
users map[int]string
}
// FindByID يبحث عن مستخدم بالمعرّف
func (r *UserRepository) FindByID(id int) (string, error) {
name, exists := r.users[id]
if !exists {
// تغليف كخطأ طبقة قاعدة البيانات
return "", &DatabaseError{
Operation: "SELECT",
Table: "users",
Err: ErrUserNotFound,
}
}
return name, nil
}
// Service طبقة خدمة الأعمال
type Service struct {
repo *UserRepository
}
// GetUser يحصل على معلومات المستخدم
func (s *Service) GetUser(id int, requireAdmin bool) (string, error) {
name, err := s.repo.FindByID(id)
if err != nil {
// التغليف للأعلى، إضافة سياق الأعمال
return "", fmt.Errorf("getting user info (id=%d): %w", id, err)
}
if requireAdmin && name != "admin" {
return "", fmt.Errorf("user %s: %w", name, ErrPermission)
}
return name, nil
}
func main() {
repo := &UserRepository{
users: map[int]string{
1: "admin",
2: "Alice",
},
}
svc := &Service{repo: repo}
// سيناريوهات الاختبار
testCases := []struct {
name string
id int
requireAdmin bool
}{
{"Find admin", 1, false},
{"Find regular user", 2, false},
{"Find non-existent user", 99, false},
{"Regular user accessing admin function", 2, true},
}
for _, tc := range testCases {
fmt.Printf("--- %s ---\n", tc.name)
user, err := svc.GetUser(tc.id, tc.requireAdmin)
if err != nil {
fmt.Printf(" Failed: %v\n", err)
// errors.Is يمكنها عبر سلسلة الأخطاء للعثور على السبب الجذري
if errors.Is(err, ErrUserNotFound) {
fmt.Println(" Action: Prompt user to check ID")
} else if errors.Is(err, ErrPermission) {
fmt.Println(" Action: Prompt user to contact admin")
}
// errors.As يمكنها استخراج أنواع أخطاء محددة من الطبقات الوسطى
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
fmt.Printf(" DB details: table=%s, operation=%s\n", dbErr.Table, dbErr.Operation)
}
} else {
fmt.Printf(" Success: User %s\n", user)
}
fmt.Println()
}
}
الناتج:
--- Find admin ---
Success: User admin
--- Find regular user ---
Success: User Alice
--- Find non-existent user ---
Failed: getting user info (id=99): database error [users.SELECT]: user not found
Action: Prompt user to check ID
DB details: table=users, operation=SELECT
--- Regular user accessing admin function ---
Failed: User Alice: insufficient permissions
Action: Prompt user to contact admin
مثال: أنواع الأخطاء المخصصة و recover في الممارسة (الصعوبة ⭐⭐⭐)
تنفيذ معالج طلبات HTTP كامل مع أنواع أخطاء مخصصة وأكواد أخطاء وware وسطية استعادة panic.
package main
import (
"errors"
"fmt"
"runtime/debug"
)
// ==================== نظام الأخطاء المخصص ====================
// AppError خطأ على مستوى التطبيق مع كود خطأ وسياق
type AppError struct {
Code int // كود خطأ الأعمال
Message string // رسالة ودية للمستخدم
Detail string // معلومات تصحيح للمطور
Cause error // السبب الأساسي
Context map[string]string // سياق إضافي
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// Is يُنفذ منطق مقارنة الأخطاء المخصص
func (e *AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return e.Code == t.Code
}
// NewAppError يُنشئ خطأ تطبيق جديد
func NewAppError(code int, message string) *AppError {
return &AppError{
Code: code,
Message: message,
Context: make(map[string]string),
}
}
// WithCause يُعيّن السبب الأساسي
func (e *AppError) WithCause(err error) *AppError {
e.Cause = err
return e
}
// WithDetail يُعيّن تفاصيل التصحيح
func (e *AppError) WithDetail(detail string) *AppError {
e.Detail = detail
return e
}
// WithContext يُضيف معلومات السياق
func (e *AppError) WithContext(key, value string) *AppError {
e.Context[key] = value
return e
}
// أكواد أخطاء مُعرَّفة مسبقًا
var (
ErrBadRequest = NewAppError(400, "bad request")
ErrUnauthorized = NewAppError(401, "unauthorized access")
ErrForbidden = NewAppError(403, "forbidden")
ErrNotFound = NewAppError(404, "resource not found")
ErrInternal = NewAppError(500, "internal server error")
)
// ==================== طبقة الأعمال المحاكية ====================
// Request يُحاكي طلب HTTP
type Request struct {
UserID int
Path string
IsAdmin bool
}
// Response يُحاكي استجابة HTTP
type Response struct {
StatusCode int
Body string
}
// validateRequest يُحقّق من معلمات الطلب
func validateRequest(req *Request) error {
if req.UserID <= 0 {
return ErrBadRequest.
WithDetail("user_id must be a positive integer").
WithContext("user_id", fmt.Sprintf("%d", req.UserID))
}
if req.Path == "" {
return ErrBadRequest.
WithDetail("path cannot be empty")
}
return nil
}
// authenticate مصادقة
func authenticate(req *Request) error {
if req.UserID == 0 {
return ErrUnauthorized.WithDetail("missing authentication credentials")
}
return nil
}
// authorize تفويض
func authorize(req *Request) error {
if !req.IsAdmin {
return ErrForbidden.
WithDetail("admin privileges required").
WithContext("user_id", fmt.Sprintf("%d", req.UserID))
}
return nil
}
// findResource يبحث عن مورد (قد يُطلق panic)
func findResource(path string) (string, error) {
// محاكاة خلل يُسبب panic
if path == "/crash" {
var p *int
*p = 1 // إشارة nil، يُطلق panic
}
resources := map[string]string{
"/users": "User list",
"/profile": "User profile",
}
data, exists := resources[path]
if !exists {
return "", ErrNotFound.
WithDetail(fmt.Sprintf("resource for path %s not found", path))
}
return data, nil
}
// handleRequest يعالج الطلب (منطق الأعمال)
func handleRequest(req *Request) (*Response, error) {
// 1. التحقق من المعلمات
if err := validateRequest(req); err != nil {
return nil, err
}
// 2. المصادقة
if err := authenticate(req); err != nil {
return nil, err
}
// 3. التفويض (فقط لمسارات الإدارة)
if req.Path == "/admin" {
if err := authorize(req); err != nil {
return nil, err
}
}
// 4. البحث عن المورد
data, err := findResource(req.Path)
if err != nil {
return nil, err
}
return &Response{
StatusCode: 200,
Body: data,
}, nil
}
// ==================== وسطية الاستعادة ====================
// RecoveryMiddleware وسطية استعادة panic
func RecoveryMiddleware(handler func(*Request) (*Response, error)) func(*Request) (*Response, error) {
return func(req *Request) (resp *Response, err error) {
defer func() {
if r := recover(); r != nil {
// تسجيل تتبع panic
stack := string(debug.Stack())
fmt.Printf("[PANIC] %v\nStack:\n%s\n", r, stack)
// إرجاع خطأ داخلي بدل الانهيار
err = ErrInternal.
WithDetail(fmt.Sprintf("panic: %v", r)).
WithCause(fmt.Errorf("panic: %v", r))
}
}()
return handler(req)
}
}
// ==================== تنسيق الأخطاء ====================
// formatError يُنسّق الخطأ إلى استجابة ودية
func formatError(err error) *Response {
var appErr *AppError
if errors.As(err, &appErr) {
msg := fmt.Sprintf("Error [%d]: %s", appErr.Code, appErr.Message)
if appErr.Detail != "" {
msg += fmt.Sprintf("\n Detail: %s", appErr.Detail)
}
if len(appErr.Context) > 0 {
msg += "\n Context:"
for k, v := range appErr.Context {
msg += fmt.Sprintf("\n %s: %s", k, v)
}
}
return &Response{
StatusCode: appErr.Code,
Body: msg,
}
}
return &Response{
StatusCode: 500,
Body: fmt.Sprintf("Unknown error: %v", err),
}
}
// ==================== الدالة الرئيسية ====================
func main() {
// تغليف المعالج بوسطية الاستعادة
safeHandler := RecoveryMiddleware(handleRequest)
// اختبار سيناريوهات متنوعة
testCases := []struct {
name string
req *Request
}{
{
name: "Normal request",
req: &Request{UserID: 1, Path: "/users", IsAdmin: true},
},
{
name: "Invalid parameters",
req: &Request{UserID: -1, Path: "/users"},
},
{
name: "Unauthenticated",
req: &Request{UserID: 0, Path: "/users"},
},
{
name: "Insufficient permissions",
req: &Request{UserID: 2, Path: "/admin", IsAdmin: false},
},
{
name: "Resource not found",
req: &Request{UserID: 1, Path: "/unknown"},
},
{
name: "Trigger panic (auto-recovery)",
req: &Request{UserID: 1, Path: "/crash"},
},
}
for _, tc := range testCases {
fmt.Printf("=== %s ===\n", tc.name)
resp, err := safeHandler(tc.req)
if err != nil {
resp = formatError(err)
}
fmt.Printf(" Status: %d\n", resp.StatusCode)
fmt.Printf(" Response: %s\n\n", resp.Body)
}
}
الناتج:
=== Normal request ===
Status: 200
Response: User list
=== Invalid parameters ===
Status: 400
Response: Error [400]: bad request
Detail: user_id must be a positive integer
Context:
user_id: -1
=== Unauthenticated ===
Status: 401
Response: Error [401]: unauthorized access
Detail: missing authentication credentials
=== Insufficient permissions ===
Status: 403
Response: Error [403]: forbidden
Detail: admin privileges required
Context:
user_id: 2
=== Resource not found ===
Status: 404
Response: Error [404]: resource not found
Detail: resource for path /unknown not found
=== Trigger panic (auto-recovery) ===
[PANIC] runtime error: invalid memory address or nil pointer dereference
Stack:
...
Status: 500
Response: Error [500]: internal server error
Detail: panic: runtime error: invalid memory address or nil pointer dereference
سيناريوهات من الواقع
السيناريو 1: التراجع عن معاملة قاعدة البيانات
في عمليات قاعدة البيانات التي تتضمن عدة خطوات، أي فشل يتطلب التراجع عن العمليات المنفذة.
package main
import (
"errors"
"fmt"
)
var (
ErrBalanceInsufficient = errors.New("insufficient balance")
ErrAccountFrozen = errors.New("account is frozen")
)
// TxError خطأ المعاملة، يُسجّل الخطوة الفاشلة
type TxError struct {
Step string
Cause error
Actions []string // الإجراءات المنفذة التي تحتاج تراجع
}
func (e *TxError) Error() string {
return fmt.Sprintf("transaction failed [step: %s]: %v", e.Step, e.Cause)
}
func (e *TxError) Unwrap() error {
return e.Cause
}
// TransferService خدمة التحويل
type TransferService struct {
balances map[string]float64
frozen map[string]bool
}
// Transfer يُنفّذ معاملة تحويل
func (s *TransferService) Transfer(from, to string, amount float64) error {
var executedSteps []string
// الخطوة 1: التحقق من الحساب المصدر
if s.frozen[from] {
return &TxError{
Step: "validate source account",
Cause: ErrAccountFrozen,
}
}
executedSteps = append(executedSteps, "lock source account")
// الخطوة 2: التحقق من الرصيد
if s.balances[from] < amount {
return &TxError{
Step: "check balance",
Cause: ErrBalanceInsufficient,
Actions: executedSteps,
}
}
executedSteps = append(executedSteps, "balance check passed")
// الخطوة 3: الخصم من الحساب المصدر
s.balances[from] -= amount
executedSteps = append(executedSteps, fmt.Sprintf("deducted %.2f", amount))
// الخطوة 4: التحقق من الحساب الهدف
if s.frozen[to] {
return &TxError{
Step: "validate target account",
Cause: ErrAccountFrozen,
Actions: executedSteps, // يحتاج تراجع المبلغ المخصوم
}
}
// الخطوة 5: الإضافة إلى الحساب الهدف
s.balances[to] += amount
fmt.Printf(" Transfer successful: %s -> %s, Amount: %.2f\n", from, to, amount)
return nil
}
func main() {
svc := &TransferService{
balances: map[string]float64{
"Alice": 1000,
"Bob": 500,
"Carol": 0,
},
frozen: map[string]bool{
"Carol": true,
},
}
tests := []struct {
name string
from string
to string
amt float64
}{
{"Normal transfer", "Alice", "Bob", 200},
{"Insufficient balance", "Alice", "Bob", 9999},
{"Target account frozen", "Alice", "Carol", 100},
}
for _, t := range tests {
fmt.Printf("--- %s ---\n", t.name)
fmt.Printf(" Before: %s=%.2f, %s=%.2f\n",
t.from, svc.balances[t.from], t.to, svc.balances[t.to])
err := svc.Transfer(t.from, t.to, t.amt)
if err != nil {
fmt.Printf(" Failed: %v\n", err)
// التحقق مما إذا كان تراجع مطلوبًا
var txErr *TxError
if errors.As(err, &txErr) && len(txErr.Actions) > 0 {
fmt.Printf(" Rollback %d executed actions:\n", len(txErr.Actions))
for _, action := range txErr.Actions {
fmt.Printf(" - Undo: %s\n", action)
}
// في المشاريع الحقيقية، نفّذ منطق التراجع هنا
// مثل: svc.balances[from] += amount
}
}
fmt.Printf(" After: %s=%.2f, %s=%.2f\n\n",
t.from, svc.balances[t.from], t.to, svc.balances[t.to])
}
}
السيناريو 2: معالجة ملفات مجمعة مع جمع الأخطاء
عند معالجة عدة ملفات، لا تتوقف عند أول فشل. بدل ذلك، اجمع جميع الأخطاء وأبلغ عنها معًا.
package main
import (
"errors"
"fmt"
"strings"
)
var (
ErrFileCorrupted = errors.New("file is corrupted")
ErrPermission = errors.New("insufficient permissions")
ErrDiskFull = errors.New("disk space insufficient")
)
// FileError خطأ ملف مفرد
type FileError struct {
Path string
Op string
Cause error
}
func (e *FileError) Error() string {
return fmt.Sprintf("[%s] %s: %v", e.Op, e.Path, e.Cause)
}
func (e *FileError) Unwrap() error {
return e.Cause
}
// BatchResult نتيجة المعالجة المجمعة
type BatchResult struct {
Success []string
Failed []FileError
}
func (r *BatchResult) HasErrors() bool {
return len(r.Failed) > 0
}
func (r *BatchResult) Summary() string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Processing complete: %d succeeded, %d failed\n",
len(r.Success), len(r.Failed)))
if len(r.Success) > 0 {
sb.WriteString("Successful files:\n")
for _, f := range r.Success {
sb.WriteString(fmt.Sprintf(" ✓ %s\n", f))
}
}
if len(r.Failed) > 0 {
sb.WriteString("Failed files:\n")
for _, e := range r.Failed {
sb.WriteString(fmt.Sprintf(" ✗ %s (%s: %v)\n", e.Path, e.Op, e.Cause))
}
}
return sb.String()
}
// processFile يُحاكي معالجة ملف مفرد
func processFile(path string) error {
// محاكاة أخطاء مختلفة محتملة
fileErrors := map[string]error{
"corrupted.txt": ErrFileCorrupted,
"secret.txt": ErrPermission,
"huge.txt": ErrDiskFull,
}
if err, exists := fileErrors[path]; exists {
return &FileError{
Path: path,
Op: "process",
Cause: err,
}
}
return nil
}
// processFiles يُعالج الملفات بشكل مجمع
func processFiles(paths []string) *BatchResult {
result := &BatchResult{}
for _, path := range paths {
err := processFile(path)
if err != nil {
result.Failed = append(result.Failed, FileError{
Path: path,
Op: "process",
Cause: errors.Unwrap(err),
})
} else {
result.Success = append(result.Success, path)
}
}
return result
}
func main() {
files := []string{
"report.pdf",
"data.csv",
"corrupted.txt",
"image.png",
"secret.txt",
"huge.txt",
"readme.md",
}
fmt.Printf("Starting to process %d files...\n\n", len(files))
result := processFiles(files)
fmt.Println(result.Summary())
// تقديم اقتراحات مختلفة بناءً على أنواع الأخطاء
for _, fe := range result.Failed {
switch {
case errors.Is(fe.Cause, ErrFileCorrupted):
fmt.Printf("Suggestion: %s needs to be restored from backup\n", fe.Path)
case errors.Is(fe.Cause, ErrPermission):
fmt.Printf("Suggestion: Please check file permissions for %s\n", fe.Path)
case errors.Is(fe.Cause, ErrDiskFull):
fmt.Printf("Suggestion: Free up disk space and retry %s\n", fe.Path)
}
}
}
❓ أسئلة شائعة
س1: متى أستخدم panic مقابل إرجاع error؟
المبدأ: استخدم error للفشل المتوقع؛ استخدم panic لأخطاء البرمجة "التي يجب ألا تحدث أبدًا."
// ✓ صحيح: إرجاع error — مدخلات المستخدم غير قابلة للتنبؤ
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divisor cannot be zero")
}
return a / b, nil
}
// ✓ صحيح: استخدام panic — هذا خلل في البرمجة، يجب ألا يُمرر nil أبدًا
func MustNotNil(v interface{}) {
if v == nil {
panic("value must not be nil, this is a caller's programming error")
}
}
القواعد العملية:
- إرجاع error: عمليات الملفات، طلبات الشبكة، التحقق من مدخلات المستخدم، فشل منطق الأعمال
- استخدام panic: فشل تهيئة
init()، أخطاء برنامج غير قابلة للتعافي،t.Fatalفي الاختبارات - أبدًا: لا تستخدم
panicلأخطاء الأعمال العادية
س2: ما الفرق بين errors.Is و errors.As؟
// errors.Is: تتحقق مما إذا كانت سلسلة الأخطاء تحتوي على خطأ رقيب محدد
// تعادل: err == target || err.Unwrap() == target || ...
errors.Is(err, ErrNotFound) // true/false
// errors.As: تستخرج نوع خطأ محدد من سلسلة الأخطاء
// كتأكيد النوع، لكنها تجتاز سلسلة الأخطاء بالكامل
var appErr *AppError
errors.As(err, &appErr) // true + تعيين، أو false
// تشبيه:
// errors.Is → "هل أنت أحمد؟" (فحص الهوية)
// errors.As → "هل أنت مبرمج؟ إذا نعم، أخبرني بمهاراتك" (استخراج معلومات)
س3: لماذا يُنصح ببدء رسائل الأخطاء بأحرف صغيرة بدون علامات ترقيم؟
عادة Go الرسمية ومجتمع المطورين:
// ✓ مُوصى به
fmt.Errorf("reading file %s failed: %w", name, err)
// الناتج: reading file config.yaml failed: permission denied
// ✗ غير مُوصى به
fmt.Errorf("reading file %s failed.: %w", name, err) // علامات ترقيم زائدة
fmt.Errorf("reading file %s failed!: %w", name, err) // تعجب زائد
fmt.Errorf("Read file %s failed: %w", name, err) // لغة غير متسقة
السبب هو أن الأخطاء غالبًا ما تُدمج في سلاسل أخطاء، والأحرف الصغيرة بدون علامات ترقيم أسهل للقراءة:
initializing database: connecting to postgres: dial tcp 127.0.0.1:5432: connection refused
س4: كيف أقارن أخطاء مغلفة؟
المقارنة المباشرة بـ == لن تعمل للأخطاء المغلفة؛ يجب استخدام errors.Is:
var ErrSentinel = errors.New("sentinel")
wrapped := fmt.Errorf("context: %w", ErrSentinel)
// ✗ خاطئ: المقارنة المباشرة تفشل
fmt.Println(wrapped == ErrSentinel) // false
// ✓ صحيح: استخدام errors.Is
fmt.Println(errors.Is(wrapped, ErrSentinel)) // true
// errors.Is تجتاز سلسلة الأخطاء بالكامل:
// wrapped → Unwrap() → ErrSentinel → تطابق!
📖 ملخص
| الموضوع | النقاط الرئيسية |
|---|---|
واجهة error |
تحتاج فقط تنفيذ أسلوب Error() string |
| إنشاء الأخطاء | errors.New للأخطاء البسيطة، fmt.Errorf + %w للأخطاء المغلفة |
| التحقق من الأخطاء | errors.Is لفحص النوع، errors.As لاستخراج النوع |
| أخطاء مخصصة | تنفيذ Error() + Unwrap()، تضمين سياق الأعمال |
panic/recover |
فقط للأخطاء غير القابلة للتعافي؛ الالتقاط بـ recover في defer |
| فلسفة الأخطاء | معالجة صريحة، فحص كل خطأ، الأخطاء قيم ليست استثناءات |
| تغليف الأخطاء | استخدم %w للحفاظ على الأخطاء الأساسية، %w لتجاهلها |
| عادة المجتمع | رسائل أخطاء بأحرف صغيرة بدون علامات ترقيم، كقيمة إرجاع أخيرة |
ثلاث قواعد ذهبية لمعالجة أخطاء Go:
- تحقق فورًا — بعد استلام
error، تحقق منerr != nilمباشرة - غلف في كل طبقة — أضف السياق في كل طبقة، استخدم
%wللحفاظ على الخطأ الأصلي - تنزّل بأناقة — تعافَ إن أمكن، أبلغ للأعلى إن لم يكن، panic كملاذ أخير فقط
📝 تمارين
تمرين 1: تنفيذ قارئ ملف مع إعادة المحاولة
اكتب دالة ReadWithRetry(path string, maxRetries int) ([]byte, error) بهذه المتطلبات:
- إعادة المحاولة حتى
maxRetriesمرات - طباعة عدد المحاولات بعد كل فشل
- عرّف خطأ رقيب
ErrMaxRetriesExceededيُرجع عند نفاد المحاولات - استخدم
fmt.Errorfلتغليف الأخطاء الأساسية مع الحفاظ على السياق
// الإطار المرجعي
var ErrMaxRetriesExceeded = errors.New("max retries exceeded")
func ReadWithRetry(path string, maxRetries int) ([]byte, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
data, err := os.ReadFile(path)
if err == nil {
return data, nil
}
lastErr = err
fmt.Printf(" Retry %d failed: %v\n", i+1, err)
}
return nil, fmt.Errorf("reading %s: %w (last error: %v)", path, ErrMaxRetriesExceeded, lastErr)
}
تمرين 2: بناء نظام مستويات أخطاء
نفّذ نظام مستويات أخطاء يدعم مستويات الأخطاء التالية:
type Level int
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
LevelFatal
)
type LeveledError struct {
Level Level
Message string
Cause error
}
المتطلبات:
- تنفيذ واجهة
error - تنفيذ أسلوب
Unwrap() error - كتابة دالة
IsLevel(err error, level Level) bool - اختبار فحص المستوى لأخطاء مختلفة
تمرين 3: تنفيذ مُجمّع أخطاء (آمن للتزامن)
اكتب نوع ErrorCollector لجمع الأخطاء من عدة goroutines في سيناريوهات متزامنة:
type ErrorCollector struct {
// الحقول التي تحتاجها
}
// Add يُضيف خطأ (آمن للتزامن)
func (c *ErrorCollector) Add(err error) { ... }
// Errors يُرجع جميع الأخطاء المجمعة
func (c *ErrorCollector) Errors() []error { ... }
// HasErrors يتحقق مما إذا كانت هناك أخطاء
func (c *ErrorCollector) HasErrors() bool { ... }
// Error يدمج جميع الأخطاء في خطأ واحد
func (c *ErrorCollector) Error() error { ... }
المتطلبات:
- استخدم
sync.Mutexأوsync.RWMutexلضمان الأمان للتزامن - اكتب اختبارات: أطلق عدة goroutines تستدعي
Addبشكل متزامن، ثم تحقق من نتائج الجمع
الدرس التالي
بعد إتمام هذا الدرس، تابع مع الدرس 11: الحزم والوحدات لتعلّم كيفية تنظيم كود Go وإدارة التبعيات ونشر حزمك الخاصة.



