إدخال/إخراج الملفات
الدرس 20: إدخال/إخراج الملفات
تشبيه من الحياة
تخيل أنك أمين مكتبة:
- فتح ملف = أخذ كتاب من الرف وفتحه للقراءة
- إنشاء ملف = أخذ دفتر فارغ وبدء كتابة محتوى
- قراءة ملف = تصفح الصفحات وقراءة النص
- كتابة ملف = استخدام قلم لكتابة محتوى جديد على الورق
- إغلاق ملف = إغلاق الكتاب وإعادته إلى الرف
- حذف ملف = إزالة الكتاب نهائياً من الرف
- تصفح مجلد = جولة في جميع الأرفف وأقسام المكتبة
تماماً كما أن للمكتبات قواعد إقراض صارمة، فإن أنظمة التشغيل لديها ضوابط أذونات للملفات. أنت بحاجة إلى "بطاقة مكتبة" (أذونات) لقراءة وكتابة الملفات، ويجب عليك "إعادتها" (إغلاقها) في الوقت المناسب عند الانتهاء، وإلا لن يتمكن القراء الآخرون من استخدامها.
المفاهيم الأساسية
توفر Go قدرات غنية لإدخال/إخراج الملفات من خلال مكتبتها القياسية، وتشمل بشكل أساسي الحزم التالية:
| الحزمة | الغرض |
|---|---|
os |
عمليات الملفات منخفضة المستوى: فتح، إنشاء، حذف، إعادة تسمية |
io |
واجهات إدخال/إخراج عامة: Reader, Writer |
bufio |
قراءة وكتابة مخزنة مؤقتاً: Scanner, Reader, Writer |
io/ioutil (متقادمة) |
تم نقل الوظائف إلى حزمة os بعد Go 1.16 |
filepath |
التعامل مع المسارات عبر الأنظمة: join, split, match |
تدفق عمليات الملف الأساسية
فتح ملف → عمليات قراءة/كتابة → إغلاق ملف
│ │
└── defer f.Close() ────┘ // ضمان الإغلاق عند خروج الدالة
مقارنة بين طرق القراءة الثلاث
| الطريقة | الخصائص | حالة الاستخدام |
|---|---|---|
os.ReadFile |
تحميل الملف كاملاً في الذاكرة دفعة واحدة | ملفات صغيرة (< 100MB) |
bufio.Scanner |
مسح سطر بسطر، صديق للذاكرة | ملفات كبيرة، معالجة سطر بسطر |
io.ReadAll |
قراءة كل شيء في []byte |
استجابات الشبكة وبيانات التدفق الأخرى |
الصياغة الأساسية والاستخدام
1. فتح وإنشاء الملفات
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
// تركيبات الأعلام الشائعة
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. قراءة الملفات
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. كتابة الملفات
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. الحذف وإعادة التسمية
// حذف ملف
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)
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. عمليات المجلدات والتصفح
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.
أمثلة
مثال: أداة نسخ الملفات (الصعوبة ⭐)
تنفيذ دالة نسخ ملف بسيطة تدعم ملفات بأي حجم.
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")
}
اكتمل النسخ، 45 بايت إجمالي
المحتوى المنسوخ: هذا محتوى الملف المصدر.
يستخدم لاختبار دالة النسخ.
مثال: محلل ملف السجلات (الصعوبة ⭐⭐)
قراءة ملف سجلات، العد حسب المستوى، وإخراج ملخص.
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")
}
========== تقرير تحليل السجلات ==========
إجمالي الأسطر: 9
ERROR: 2 (22.2%)
WARNING: 2 (22.2%)
INFO: 3 (33.3%)
DEBUG: 2 (22.2%)
===========================================
مثال: أداة مزامنة المجلدات (الصعوبة ⭐⭐⭐)
تنفيذ دالة مزامنة مجلدات بسيطة: مسح المجلد المصدر ونسخ الملفات الجديدة أو المعدّلة إلى مجلد الوجهة.
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")
}
المزامنة: 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: إعادة تحميل التكوين الساخنة
مراقبة تغييرات ملف التكوين وإعادة التحميل تلقائياً.
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)
}
الخادم: 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: أداة إعادة تسمية الملفات بالدفعة
إعادة تسمية الملفات في مجلد وفقاً لقواعد.
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")
}
=== القاعدة 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: لماذا يرتفع استخدام الذاكرة عند قراءة الملفات الكبيرة؟
// ❌ خطأ: تحميل الملف كاملاً في الذاكرة
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؟
// ❌ قد يتسبب في 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؟
// ❌ فاصل مسار مُ⚚د
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: ماذا أفعل عند فقدان البيانات أثناء كتابة الملفات؟
// السبب: استخدام 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 |
نسخ بالتدفق، يدير المخزن المؤقت تلقائياً |
المبادئ الأساسية:
- دائماً
defer f.Close()— بعد فحص الخطأ - الملفات الصغيرة
os.ReadFile، الملفات الكبيرةbufio.Scanner - دمج المسارات بـ
filepath.Join— لا تدمج الفواصل يدوياً bufio.Writerيجب أن يكونFlush()— وإلا قد تُفقد البيانات- معالجة الأخطاء لا يمكن تخطيها — أخطاء عمليات الملفات مهمة بشكل خاص
📝 تمارين
التمرين 1: عداد الكلمات
اكتب برنامجاً يقرأ ملف نصي، يعد عدد الكلمات والأسطر والأحرف، ويُخرج النتائج.
// تلميحات:
// - استخدم bufio.Scanner للقراءة سطر بسطر
// - استخدم strings.Fields() لتقسيم كل سطر إلى كلمات
// - اعد المقاييس الثلاثة ونسق المخرجات
التمرين 2: CSV إلى JSON
اكتب برنامجاً يقرأ ملف CSV (الصف الأول عناوين)، ويحوله إلى صيغة مصفوفة JSON، ويكتبه في ملف جديد.
// تلميحات:
// - استخدم bufio.Scanner لقراءة CSV سطر بسطر
// - الصف الأول يعمل كمفاتيح لكائنات JSON
// - الصفوف التالية هي القيم، تقسيم بـ strings.Split
// - استخدم encoding/json للتسلسل
التمرين 3: حاسب حجم المجلد
اكتب برنامجاً يحسب الحجم الإجمالي لمجلد محدد تتابعاً ويجمع الإحصائيات حسب نوع الملف (الامتداد).
// تلميحات:
// - استخدم filepath.WalkDir لتصفح المجلد
// - استخدم map[string]int64 لتراكم الحجم حسب الامتداد
// - استخدم humanize للمخرجات المنسقة (مثل 1.5MB، 230KB)
// - تعامل مع الملفات بدون امتداد (صنفها كـ "بدون امتداد")
الدرس التالي
في الدرس التالي، سنتعلم معالجة JSON — كيفية تحليل وإنشاء بيانات JSON في Go، وهي مهارة أساسية لبناء واجهات برمجة التطبيقات والتعامل مع ملفات التكوين.



