إدخال/إخراج الملفات

الدرس 20: إدخال/إخراج الملفات

تشبيه من الحياة

تخيل أنك أمين مكتبة:

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


المفاهيم الأساسية

توفر Go قدرات غنية لإدخال/إخراج الملفات من خلال مكتبتها القياسية، وتشمل بشكل أساسي الحزم التالية:

الحزمة الغرض
os عمليات الملفات منخفضة المستوى: فتح، إنشاء، حذف، إعادة تسمية
io واجهات إدخال/إخراج عامة: Reader, Writer
bufio قراءة وكتابة مخزنة مؤقتاً: Scanner, Reader, Writer
io/ioutil (متقادمة) تم نقل الوظائف إلى حزمة os بعد Go 1.16
filepath التعامل مع المسارات عبر الأنظمة: join, split, match

تدفق عمليات الملف الأساسية

TEXT
فتح ملف → عمليات قراءة/كتابة → إغلاق ملف
   │                       │
   └── defer f.Close() ────┘   // ضمان الإغلاق عند خروج الدالة

مقارنة بين طرق القراءة الثلاث

الطريقة الخصائص حالة الاستخدام
os.ReadFile تحميل الملف كاملاً في الذاكرة دفعة واحدة ملفات صغيرة (< 100MB)
bufio.Scanner مسح سطر بسطر، صديق للذاكرة ملفات كبيرة، معالجة سطر بسطر
io.ReadAll قراءة كل شيء في []byte استجابات الشبكة وبيانات التدفق الأخرى

الصياغة الأساسية والاستخدام

1. فتح وإنشاء الملفات

GO
package main

import (
    "fmt"
    "os"
)

func main() {
    // فتح ملف في وضع القراءة فقط (يجب أن يكون الملف موجوداً)
    f, err := os.Open("data.txt")
    if err != nil {
        fmt.Println("فشل الفتح:", err)
        return
    }
    defer f.Close() // 💡 استخدم دائماً defer لإغلاق الملفات

    // فتح ملف في وضع القراءة والكتابة (يجب أن يكون الملف موجوداً)
    f2, err := os.OpenFile("data.txt", os.O_RDWR, 0644)
    if err != nil {
        fmt.Println("فشل الفتح:", err)
        return
    }
    defer f2.Close()

    // إنشاء ملف جديد (قطع إذا كان موجوداً)
    f3, err := os.Create("newfile.txt")
    if err != nil {
        fmt.Println("فشل الإنشاء:", err)
        return
    }
    defer f3.Close()
}
💡 نصيحة: os.Open تعادل os.OpenFile(name, os.O_RDONLY, 0) — قراءة فقط، بدون كتابة.

2. أعلام OpenFile

GO
// تركيبات الأعلام الشائعة
os.O_RDONLY  // قراءة فقط
os.O_WRONLY  // كتابة فقط
os.O_RDWR    // قراءة وكتابة
os.O_APPEND  // وضع الإلحاق
os.O_CREATE  // إنشاء إذا لم يكن الملف موجوداً
os.O_TRUNC   // قطع عند الفتح (مسح المحتوى)

// كتابة بإلحاق
f, err := os.OpenFile("log.txt",
    os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
💡 نصيحة: الصلاحية 0644 تعني أن المالك يمكنه القراءة والكتابة، والآخرون يمكنهم القراءة فقط (أنظمة Unix).

3. قراءة الملفات

GO
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    // ===== الطريقة 1: قراءة الملف كاملاً دفعة واحدة =====
    data, err := os.ReadFile("config.txt")
    if err != nil {
        fmt.Println("فشل القراءة:", err)
        return
    }
    fmt.Println(string(data))

    // ===== الطريقة 2: قراءة سطر بسطر (موصى بها للملفات الكبيرة) =====
    file, err := os.Open("access.log")
    if err != nil {
        fmt.Println("فشل الفتح:", err)
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    lineNum := 0
    for scanner.Scan() { // مسح سطر بسطر
        lineNum++
        fmt.Printf("السطر %d: %s\n", lineNum, scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        fmt.Println("خطأ المسح:", err)
    }
}
💡 نصيحة: bufio.Scanner لديه أقصى طول رمز افتراضي 64KB. للأسطر الطويلة جداً، استدعي scanner.Buffer() لزيادة حجم المخزن المؤقت.

4. كتابة الملفات

GO
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    // ===== الطريقة 1: الكتابة دفعة واحدة =====
    err := os.WriteFile("output.txt", []byte("Hello, Go!\n"), 0644)
    if err != nil {
        fmt.Println("فشل الكتابة:", err)
        return
    }

    // ===== الطريقة 2: استخدام bufio.Writer (مخزن مؤقت) =====
    f, err := os.Create("buffered.txt")
    if err != nil {
        fmt.Println("فشل الإنشاء:", err)
        return
    }
    defer f.Close()

    writer := bufio.NewWriter(f)
    for i := 1; i <= 5; i++ {
        fmt.Fprintf(writer, "السطر %d محتوى\n", i)
    }
    writer.Flush() // 💡 يجب Flush، وإلا تبقى البيانات في المخزن المؤقت ولا تُكتب على القرص

    // ===== الطريقة 3: الكتابة المباشرة =====
    f2, _ := os.Create("direct.txt")
    defer f2.Close()
    f2.WriteString("كتابة نص مباشرة\n")
    f2.Write([]byte("كتابة شريحة بايت مباشرة\n"))
}
💡 نصيحة: bufio.Writer يحسّن الأداء بشكل كبير للكتابات الصغيرة المتكررة لأنه يقلل من عدد استدعاءات النظام. لكن يجب دائماً استدعاء Flush() في النهاية.

5. الحذف وإعادة التسمية

GO
// حذف ملف
err := os.Remove("temp.txt")
if err != nil {
    fmt.Println("فشل الحذف:", err)
}

// حذف مجلد وجميع محتوياته
err = os.RemoveAll("temp_dir/")

// إعادة تسمية / نقل ملف
err = os.Rename("old.txt", "new.txt")

6. التعامل مع المسارات (حزمة filepath)

GO
package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    // دمج المسارات (آمن عبر الأنظمة)
    p := filepath.Join("data", "logs", "app.log")
    fmt.Println(p) // data\logs\app.log (Windows) أو data/logs/app.log (Linux)

    // تقسيم المسار
    dir, file := filepath.Split("/home/user/doc.txt")
    fmt.Println("المجلد:", dir)   // /home/user/
    fmt.Println("الملف:", file)   // doc.txt

    // الحصول على الامتداد
    ext := filepath.Ext("report.pdf")
    fmt.Println(ext) // .pdf

    // الحصول على اسم الملف بدون امتداد
    name := filepath.Base("report.pdf")
    fmt.Println(name) // report.pdf

    // المسار المطلق
    abs, _ := filepath.Abs("relative/path")
    fmt.Println(abs)

    // مطابقة المسارات (نمط glob)
    matched, _ := filepath.Match("*.go", "main.go")
    fmt.Println(matched) // true
}
💡 نصيحة: استخدم دائماً filepath.Join لدمج المسارات — لا تدمج يدوياً / أو \، وإلا سيكون لديك مشاكل عبر الأنظمة.

7. عمليات المجلدات والتصفح

GO
package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    // إنشاء مجلدات
    os.Mkdir("logs", 0755)
    os.MkdirAll("logs/2024/01", 0755) // إنشاء تتابعي

    // قراءة محتويات المجلد
    entries, err := os.ReadDir(".")
    if err != nil {
        fmt.Println("فشل قراءة المجلد:", err)
        return
    }
    for _, entry := range entries {
        info, _ := entry.Info()
        fmt.Printf("%-10s %8d بايت  %s\n",
            entry.Name(), info.Size(), info.Mode())
    }

    // تصفح تتابعي لشجرة المجلدات
    fmt.Println("\n=== التصفح التتابعي ===")
    filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        prefix := "📄"
        if info.IsDir() {
            prefix = "📁"
        }
        fmt.Printf("%s %s\n", prefix, path)
        return nil
    })
}
💡 نصيحة: filepath.Walk يزور كل ملف. لأشجار المجلدات الكبيرة جداً، استخدم filepath.WalkDir (Go 1.16+) لأداء أفضل، لأنه يقلل من استدعاءات stat.


أمثلة

مثال: أداة نسخ الملفات (الصعوبة ⭐)

تنفيذ دالة نسخ ملف بسيطة تدعم ملفات بأي حجم.

GO
package main

import (
    "fmt"
    "io"
    "os"
)

// copyFile تنسخ الملف المصدر إلى المسار الوجهة
func copyFile(src, dst string) (int64, error) {
    // فتح الملف المصدر
    sourceFile, err := os.Open(src)
    if err != nil {
        return 0, fmt.Errorf("فشل فتح الملف المصدر: %w", err)
    }
    defer sourceFile.Close()

    // الحصول على معلومات الملف المصدر (لتعيين الصلاحيات)
    sourceInfo, err := sourceFile.Stat()
    if err != nil {
        return 0, fmt.Errorf("فشل الحصول على معلومات الملف: %w", err)
    }

    // إنشاء ملف الوجهة (يرث صلاحيات الملف المصدر)
    destFile, err := os.OpenFile(dst,
        os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
        sourceInfo.Mode())
    if err != nil {
        return 0, fmt.Errorf("فشل إنشاء ملف الوجهة: %w", err)
    }
    defer destFile.Close()

    // استخدام io.Copy للنسخ بالتدفق (يتعامل مع المخزن المؤقت تلقائياً)
    bytesWritten, err := io.Copy(destFile, sourceFile)
    if err != nil {
        return 0, fmt.Errorf("فشل نسخ البيانات: %w", err)
    }

    return bytesWritten, nil
}

func main() {
    // إنشاء ملف اختبار
    os.WriteFile("source.txt", []byte("هذا محتوى الملف المصدر.\nيستخدم لاختبار دالة النسخ.\n"), 0644)

    // تنفيذ النسخ
    n, err := copyFile("source.txt", "copy.txt")
    if err != nil {
        fmt.Println("خطأ:", err)
        return
    }
    fmt.Printf("اكتمل النسخ، %d بايت إجمالي\n", n)

    // التحقق من النتيجة
    data, _ := os.ReadFile("copy.txt")
    fmt.Println("المحتوى المنسوخ:", string(data))

    // التنظيف
    os.Remove("source.txt")
    os.Remove("copy.txt")
}
▶ جرّب الكود
TEXT
اكتمل النسخ، 45 بايت إجمالي
المحتوى المنسوخ: هذا محتوى الملف المصدر.
يستخدم لاختبار دالة النسخ.

مثال: محلل ملف السجلات (الصعوبة ⭐⭐)

قراءة ملف سجلات، العد حسب المستوى، وإخراج ملخص.

GO
package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

// LogStats يحتفظ بإحصائيات السجلات
type LogStats struct {
    Total   int
    Error   int
    Warning int
    Info    int
    Debug   int
}

// analyzeLog يحلل ملف السجلات ويعيد الإحصائيات
func analyzeLog(filename string) (*LogStats, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, fmt.Errorf("فشل فتح ملف السجلات: %w", err)
    }
    defer file.Close()

    stats := &LogStats{}
    scanner := bufio.NewScanner(file)

    // زيادة حجم المخزن المؤقت للأسطر الطويلة جداً
    scanner.Buffer(make([]byte, 1024*1024), 1024*1024)

    for scanner.Scan() {
        line := scanner.Text()
        stats.Total++

        switch {
        case strings.Contains(line, "[ERROR]"):
            stats.Error++
        case strings.Contains(line, "[WARNING]"):
            stats.Warning++
        case strings.Contains(line, "[INFO]"):
            stats.Info++
        case strings.Contains(line, "[DEBUG]"):
            stats.Debug++
        }
    }

    if err := scanner.Err(); err != nil {
        return nil, fmt.Errorf("خطأ في قراءة السجل: %w", err)
    }

    return stats, nil
}

func main() {
    // إنشاء ملف سجلات محاكاة
    logContent := `2024-01-15 08:00:01 [INFO] Service started successfully
2024-01-15 08:00:05 [DEBUG] Loading configuration file
2024-01-15 08:01:10 [WARNING] Disk space below 80%
2024-01-15 08:02:30 [ERROR] Database connection timeout
2024-01-15 08:02:35 [INFO] Retrying database connection
2024-01-15 08:03:00 [ERROR] Authentication failed: user admin
2024-01-15 08:03:01 [INFO] Request processing complete
2024-01-15 08:04:00 [DEBUG] Cache hit rate 95%
2024-01-15 08:05:00 [WARNING] API response time exceeded 2s
`
    os.WriteFile("app.log", []byte(logContent), 0644)

    // تحليل السجلات
    stats, err := analyzeLog("app.log")
    if err != nil {
        fmt.Println("خطأ:", err)
        return
    }

    // إخراج التقرير
    fmt.Println("========== تقرير تحليل السجلات ==========")
    fmt.Printf("إجمالي الأسطر: %d\n", stats.Total)
    fmt.Printf("ERROR:   %d (%.1f%%)\n", stats.Error,
        float64(stats.Error)/float64(stats.Total)*100)
    fmt.Printf("WARNING: %d (%.1f%%)\n", stats.Warning,
        float64(stats.Warning)/float64(stats.Total)*100)
    fmt.Printf("INFO:    %d (%.1f%%)\n", stats.Info,
        float64(stats.Info)/float64(stats.Total)*100)
    fmt.Printf("DEBUG:   %d (%.1f%%)\n", stats.Debug,
        float64(stats.Debug)/float64(stats.Total)*100)
    fmt.Println("===========================================")

    // التنظيف
    os.Remove("app.log")
}
▶ جرّب الكود
TEXT
========== تقرير تحليل السجلات ==========
إجمالي الأسطر: 9
ERROR:   2 (22.2%)
WARNING: 2 (22.2%)
INFO:    3 (33.3%)
DEBUG:   2 (22.2%)
===========================================

مثال: أداة مزامنة المجلدات (الصعوبة ⭐⭐⭐)

تنفيذ دالة مزامنة مجلدات بسيطة: مسح المجلد المصدر ونسخ الملفات الجديدة أو المعدّلة إلى مجلد الوجهة.

GO
package main

import (
    "fmt"
    "io"
    "os"
    "path/filepath"
    "time"
)

// FileInfo معلومات ملف مخزنة مؤقتاً
type FileInfo struct {
    Path    string
    ModTime time.Time
    Size    int64
}

// scanDir يمسح مجلداً ويعيد خريطة معلومات الملفات
func scanDir(dir string) (map[string]FileInfo, error) {
    files := make(map[string]FileInfo)

    err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if info.IsDir() {
            return nil
        }

        // حساب المسار النسبي كمفتاح
        relPath, err := filepath.Rel(dir, path)
        if err != nil {
            return err
        }

        files[relPath] = FileInfo{
            Path:    path,
            ModTime: info.ModTime(),
            Size:    info.Size(),
        }
        return nil
    })

    return files, err
}

// copyFileData تنسخ ملفاً واحداً
func copyFileData(src, dst string) error {
    srcFile, err := os.Open(src)
    if err != nil {
        return err
    }
    defer srcFile.Close()

    // التأكد من وجود مجلد الوجهة
    dstDir := filepath.Dir(dst)
    if err := os.MkdirAll(dstDir, 0755); err != nil {
        return err
    }

    dstFile, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer dstFile.Close()

    _, err = io.Copy(dstFile, srcFile)
    return err
}

// syncDir تزامن المجلد المصدر إلى مجلد الوجهة
func syncDir(src, dst string) error {
    fmt.Printf("المزامنة: %s → %s\n\n", src, dst)

    // مسح كلا المجلدين
    srcFiles, err := scanDir(src)
    if err != nil {
        return fmt.Errorf("فشل مسح المجلد المصدر: %w", err)
    }

    dstFiles, err := scanDir(dst)
    if err != nil && !os.IsNotExist(err) {
        return fmt.Errorf("فشل مسح مجلد الوجهة: %w", err)
    }

    copied, updated, skipped := 0, 0, 0

    // التكرار على ملفات المجلد المصدر
    for relPath, srcInfo := range srcFiles {
        dstPath := filepath.Join(dst, relPath)

        dstInfo, exists := dstFiles[relPath]

        switch {
        case !exists:
            // ملف جديد، نسخه
            fmt.Printf("  [جديد] %s\n", relPath)
            if err := copyFileData(srcInfo.Path, dstPath); err != nil {
                return fmt.Errorf("فشل نسخ %s: %w", relPath, err)
            }
            copied++

        case srcInfo.ModTime.After(dstInfo.ModTime) || srcInfo.Size != dstInfo.Size:
            // ملف معدل، تحديثه
            fmt.Printf("  [محدث] %s\n", relPath)
            if err := copyFileData(srcInfo.Path, dstPath); err != nil {
                return fmt.Errorf("فشل تحديث %s: %w", relPath, err)
            }
            updated++

        default:
            // ملف دون تغيير، تخطيه
            skipped++
        }
    }

    fmt.Printf("\nاكتملت المزامنة: %d جديد، %d محدث، %d متخطى\n",
        copied, updated, skipped)
    return nil
}

func main() {
    // إنشاء هيكل مجلدات اختبار
    os.MkdirAll("src_dir/subdir", 0755)
    os.WriteFile("src_dir/main.go", []byte("package main\n"), 0644)
    os.WriteFile("src_dir/readme.txt", []byte("README\n"), 0644)
    os.WriteFile("src_dir/subdir/util.go", []byte("package util\n"), 0644)

    // إنشاء مجلد الوجهة (مع بعض الملفات)
    os.MkdirAll("dst_dir", 0755)
    os.WriteFile("dst_dir/readme.txt", []byte("Old README\n"), 0644)
    os.WriteFile("dst_dir/old.txt", []byte("هذا الملف ليس في المجلد المصدر\n"), 0644)

    // تنفيذ المزامنة
    err := syncDir("src_dir", "dst_dir")
    if err != nil {
        fmt.Println("خطأ المزامنة:", err)
        return
    }

    // التحقق من النتيجة
    fmt.Println("\n=== محتويات مجلد الوجهة ===")
    filepath.Walk("dst_dir", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        relPath, _ := filepath.Rel("dst_dir", path)
        prefix := "📁"
        if !info.IsDir() {
            prefix = "📄"
        }
        fmt.Printf("  %s %s\n", prefix, relPath)
        return nil
    })

    // التنظيف
    os.RemoveAll("src_dir")
    os.RemoveAll("dst_dir")
}
▶ جرّب الكود
TEXT
المزامنة: src_dir → dst_dir

  [جديد] main.go
  [محدث] readme.txt
  [جديد] subdir\util.go

اكتملت المزامنة: 2 جديد، 1 محدث، 0 متخطى

=== محتويات مجلد الوجهة ===
  📁 .
  📁 subdir
  📄 subdir\util.go
  📄 main.go
  📄 old.txt
  📄 readme.txt

سيناريوهات التطبيق العملي

السيناريو 1: إعادة تحميل التكوين الساخنة

مراقبة تغييرات ملف التكوين وإعادة التحميل تلقائياً.

GO
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"
)

// Config تكوين التطبيق
type Config struct {
    Server   ServerConfig   `json:"server"`
    Database DatabaseConfig `json:"database"`
}

type ServerConfig struct {
    Port int    `json:"port"`
    Host string `json:"host"`
}

type DatabaseConfig struct {
    DSN         string `json:"dsn"`
    MaxOpenConn int    `json:"max_open_conn"`
}

// loadConfig يحمّل التكوين من ملف JSON
func loadConfig(filename string) (*Config, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, fmt.Errorf("فشل قراءة ملف التكوين: %w", err)
    }

    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("فشل تحليل التكوين: %w", err)
    }

    return &config, nil
}

// watchConfig يراقب تغييرات ملف التكوين ويُعيد التحميل
func watchConfig(filename string, interval time.Duration, callback func(*Config)) {
    var lastModTime time.Time

    // التحميل الأولي
    info, err := os.Stat(filename)
    if err == nil {
        lastModTime = info.ModTime()
        if config, err := loadConfig(filename); err == nil {
            callback(config)
        }
    }

    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for range ticker.C {
        info, err := os.Stat(filename)
        if err != nil {
            fmt.Printf("فشل التحقق من ملف التكوين: %v\n", err)
            continue
        }

        if info.ModTime().After(lastModTime) {
            fmt.Printf("[%s] تم اكتشاف تغيير في ملف التكوين، إعادة التحميل...\n",
                time.Now().Format("15:04:05"))

            config, err := loadConfig(filename)
            if err != nil {
                fmt.Printf("فشل إعادة التحميل: %v\n", err)
                continue
            }

            lastModTime = info.ModTime()
            callback(config)
        }
    }
}

func main() {
    // إنشاء التكوين الأولي
    configJSON := `{
    "server": {
        "port": 8080,
        "host": "localhost"
    },
    "database": {
        "dsn": "user:pass@tcp(localhost:3306)/mydb",
        "max_open_conn": 25
    }
}`
    os.WriteFile("config.json", []byte(configJSON), 0644)
    defer os.Remove("config.json")

    // بدء مراقبة التكوين
    go watchConfig("config.json", 2*time.Second, func(config *Config) {
        fmt.Printf("  الخادم: %s:%d\n", config.Server.Host, config.Server.Port)
        fmt.Printf("  قاعدة البيانات: %s (أقصى اتصالات: %d)\n",
            config.Database.DSN, config.Database.MaxOpenConn)
    })

    // محاكاة التشغيل
    time.Sleep(3 * time.Second)

    // محاكاة تحديث التكوين
    fmt.Println("\n>> تحديث ملف التكوين...")
    updatedJSON := `{
    "server": {
        "port": 9090,
        "host": "0.0.0.0"
    },
    "database": {
        "dsn": "user:pass@tcp(db-host:3306)/mydb",
        "max_open_conn": 50
    }
}`
    os.WriteFile("config.json", []byte(updatedJSON), 0644)

    // انتظار اكتشاف التغيير
    time.Sleep(5 * time.Second)
}
TEXT
  الخادم: localhost:8080
  قاعدة البيانات: user:pass@tcp(localhost:3306)/mydb (أقصى اتصالات: 25)

>> تحديث ملف التكوين...
[14:30:02] تم اكتشاف تغيير في ملف التكوين، إعادة التحميل...
  الخادم: 0.0.0.0:9090
  قاعدة البيانات: user:pass@tcp(db-host:3306)/mydb (أقصى اتصالات: 50)

السيناريو 2: أداة إعادة تسمية الملفات بالدفعة

إعادة تسمية الملفات في مجلد وفقاً لقواعد.

GO
package main

import (
    "fmt"
    "os"
    "path/filepath"
    "strings"
)

// RenameRule قاعدة إعادة التسمية
type RenameRule struct {
    Find    string // النص المراد البحث عنه
    Replace string // النص البديل
}

// batchRename يعيد تسمية الملفات بالدفعة
func batchRename(dir string, rule RenameRule) ([]string, error) {
    entries, err := os.ReadDir(dir)
    if err != nil {
        return nil, fmt.Errorf("فشل قراءة المجلد: %w", err)
    }

    var renamed []string

    for _, entry := range entries {
        if entry.IsDir() {
            continue
        }

        oldName := entry.Name()
        newName := strings.ReplaceAll(oldName, rule.Find, rule.Replace)

        if oldName == newName {
            continue // لا حاجة لإعادة التسمية
        }

        oldPath := filepath.Join(dir, oldName)
        newPath := filepath.Join(dir, newName)

        // التحقق مما إذا كان الملف الهدف موجوداً بالفعل
        if _, err := os.Stat(newPath); err == nil {
            fmt.Printf("  [متخطى] %s → %s (الهدف موجود بالفعل)\n", oldName, newName)
            continue
        }

        if err := os.Rename(oldPath, newPath); err != nil {
            fmt.Printf("  [خطأ] %s: %v\n", oldName, err)
            continue
        }

        fmt.Printf("  [معاد تسميته] %s → %s\n", oldName, newName)
        renamed = append(renamed, newName)
    }

    return renamed, nil
}

func main() {
    // إنشاء ملفات اختبار
    os.MkdirAll("photos", 0755)
    testFiles := []string{
        "IMG_20240115_001.jpg",
        "IMG_20240115_002.jpg",
        "IMG_20240115_003.jpg",
        "IMG_20240116_001.jpg",
        "IMG_20240116_002.jpg",
        "notes.txt",
    }
    for _, name := range testFiles {
        os.WriteFile(filepath.Join("photos", name), []byte(""), 0644)
    }

    // القاعدة 1: استبدال البادئة
    fmt.Println("=== القاعدة 1: IMG → Photo ===")
    rule1 := RenameRule{Find: "IMG_", Replace: "Photo_"}
    renamed, _ := batchRename("photos", rule1)
    fmt.Printf("تمت إعادة تسمية %d ملفاً\n\n", len(renamed))

    // القاعدة 2: إضافة بادئة تاريخ
    fmt.Println("=== القاعدة 2: Photo_ → 2024_Vacation_ ===")
    rule2 := RenameRule{Find: "Photo_", Replace: "2024_Vacation_"}
    renamed, _ = batchRename("photos", rule2)
    fmt.Printf("تمت إعادة تسمية %d ملفاً\n", len(renamed))

    // عرض النتائج النهائية
    fmt.Println("\n=== قائمة الملفات النهائية ===")
    entries, _ := os.ReadDir("photos")
    for _, entry := range entries {
        fmt.Printf("  %s\n", entry.Name())
    }

    // التنظيف
    os.RemoveAll("photos")
}
TEXT
=== القاعدة 1: IMG → Photo ===
  [معاد تسميته] IMG_20240115_001.jpg → Photo_20240115_001.jpg
  [معاد تسميته] IMG_20240115_002.jpg → Photo_20240115_002.jpg
  [معاد تسميته] IMG_20240115_003.jpg → Photo_20240115_003.jpg
  [معاد تسميته] IMG_20240116_001.jpg → Photo_20240116_001.jpg
  [معاد تسميته] IMG_20240116_002.jpg → Photo_20240116_002.jpg
تمت إعادة تسمية 5 ملفاً

=== القاعدة 2: Photo_ → 2024_Vacation_ ===
  [معاد تسميته] Photo_20240115_001.jpg → 2024_Vacation_20240115_001.jpg
  [معاد تسميته] Photo_20240115_002.jpg → 2024_Vacation_20240115_002.jpg
  [معاد تسميته] Photo_20240115_003.jpg → 2024_Vacation_20240115_003.jpg
  [معاد تسميته] Photo_20240116_001.jpg → 2024_Vacation_20240116_001.jpg
  [معاد تسميته] Photo_20240116_002.jpg → 2024_Vacation_20240116_002.jpg
تمت إعادة تسمية 5 ملفاً

=== قائمة الملفات النهائية ===
  2024_Vacation_20240115_001.jpg
  2024_Vacation_20240115_002.jpg
  2024_Vacation_20240115_003.jpg
  2024_Vacation_20240116_001.jpg
  2024_Vacation_20240116_002.jpg
  notes.txt

❓ أسئلة شائعة

س1: لماذا يرتفع استخدام الذاكرة عند قراءة الملفات الكبيرة؟

GO
// ❌ خطأ: تحميل الملف كاملاً في الذاكرة
data, _ := os.ReadFile("huge.log") // ملف 10GB → انفجار الذاكرة

// ✅ صحيح: قراءة سطر بسطر
file, _ := os.Open("huge.log")
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    // معالجة سطر واحد في كل مرة، استخدام ذاكرة أدنى
    processLine(line)
}

نقطة أساسية: os.ReadFile مناسب للملفات الصغيرة (< 100MB). للملفات الكبيرة، استخدم دائماً bufio.Scanner للمعالجة سطر بسطر / بكتل.

س2: ماذا يحدث إذا وُضع defer f.Close() قبل فحص err != nil؟

GO
// ❌ قد يتسبب في panic مؤشر فارغ
f, err := os.Open("file.txt")
defer f.Close() // إذا فشل الفتح، يكون f فارغاً، سيحدث panic عند Close
if err != nil {
    return err
}

// ✅ صحيح: فحص الخطأ أولاً
f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close() // يضمن أن f ليس فارغاً

س3: كيف أتعامل مع اختلافات المسارات بين Windows و Linux؟

GO
// ❌ فاصل مسار مُ⚚د
path := "data" + "/" + "file.txt"     // يعمل على Linux، محفوف بالمخاطر على Windows
path := "data" + "\\" + "file.txt"    // يعمل على Windows، يفشل على Linux

// ✅ استخدم filepath.Join
path := filepath.Join("data", "file.txt")

// ✅ استخدم النصوص الخام (مسارات Windows)
path := `C:\Users\admin\file.txt`

// ✅ الحصول على مسار نسبي من مسار مطلق
rel, _ := filepath.Rel("/home/user", "/home/user/docs/file.txt")
// rel = "docs/file.txt"

س4: ماذا أفعل عند فقدان البيانات أثناء كتابة الملفات؟

GO
// السبب: استخدام bufio.Writer ولكن نسيان Flush
writer := bufio.NewWriter(f)
writer.WriteString("بيانات مهمة")
// البرنامج يتعطل أو يخرج → البيانات لا تزال في المخزن المؤقت، لم تُكتب على القرص

// ✅ الحل 1: دائماً defer Flush
defer writer.Flush()

// ✅ الحل 2: كتابة البيانات الحساسة مباشرة
f.WriteString("بيانات مهمة") // كتابة مباشرة، تتجاوز المخزن المؤقت

// ✅ الحل 3: المزامنة على القرص فوراً بعد الكتابة
f.Sync() // يستدعي fsync system call

📖 ملخص

غطى هذا الدرس المعرفة الأساسية لإدخال/إخراج الملفات في Go:

نقطة المعرفة الاستنتاج الرئيسي
os.Open/Create/Remove عمليات ملفات منخفضة المستوى، تتطلب defer Close() يدوياً
os.ReadFile/WriteFile طريقة القراءة/الكتابة الموصى بها لمرة واحدة في Go 1.16+
bufio.Scanner الخيار الأفضل لقراءة الملفات الكبيرة سطر بسطر
bufio.Writer تحسين أداء للكتابات الصغيرة المتكررة، لا تنسَ Flush()
filepath.Join الطريقة الصحيحة لدمج المسارات عبر الأنظمة
filepath.Walk/WalkDir تصفح تتابعي لأشجار المجلدات
io.Copy نسخ بالتدفق، يدير المخزن المؤقت تلقائياً

المبادئ الأساسية:

  1. دائماً defer f.Close() — بعد فحص الخطأ
  2. الملفات الصغيرة os.ReadFile، الملفات الكبيرة bufio.Scanner
  3. دمج المسارات بـ filepath.Join — لا تدمج الفواصل يدوياً
  4. bufio.Writer يجب أن يكون Flush() — وإلا قد تُفقد البيانات
  5. معالجة الأخطاء لا يمكن تخطيها — أخطاء عمليات الملفات مهمة بشكل خاص

📝 تمارين

التمرين 1: عداد الكلمات

اكتب برنامجاً يقرأ ملف نصي، يعد عدد الكلمات والأسطر والأحرف، ويُخرج النتائج.

GO
// تلميحات:
// - استخدم bufio.Scanner للقراءة سطر بسطر
// - استخدم strings.Fields() لتقسيم كل سطر إلى كلمات
// - اعد المقاييس الثلاثة ونسق المخرجات

التمرين 2: CSV إلى JSON

اكتب برنامجاً يقرأ ملف CSV (الصف الأول عناوين)، ويحوله إلى صيغة مصفوفة JSON، ويكتبه في ملف جديد.

GO
// تلميحات:
// - استخدم bufio.Scanner لقراءة CSV سطر بسطر
// - الصف الأول يعمل كمفاتيح لكائنات JSON
// - الصفوف التالية هي القيم، تقسيم بـ strings.Split
// - استخدم encoding/json للتسلسل

التمرين 3: حاسب حجم المجلد

اكتب برنامجاً يحسب الحجم الإجمالي لمجلد محدد تتابعاً ويجمع الإحصائيات حسب نوع الملف (الامتداد).

GO
// تلميحات:
// - استخدم filepath.WalkDir لتصفح المجلد
// - استخدم map[string]int64 لتراكم الحجم حسب الامتداد
// - استخدم humanize للمخرجات المنسقة (مثل 1.5MB، 230KB)
// - تعامل مع الملفات بدون امتداد (صنفها كـ "بدون امتداد")

الدرس التالي

في الدرس التالي، سنتعلم معالجة JSON — كيفية تحليل وإنشاء بيانات JSON في Go، وهي مهارة أساسية لبناء واجهات برمجة التطبيقات والتعامل مع ملفات التكوين.

👉 الدرس 21: معالجة JSON

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%