Skip to content

الملفات

توفر لغة Go مكتبات قياسية لمعالجة الملفات تقريباً:

  • مكتبة os، المسؤولة عن التنفيذ المحدد للتفاعل مع نظام ملفات نظام التشغيل
  • مكتبة io، طبقة تجريدية للقراءة والكتابة IO
  • مكتبة fs، طبقة تجريدية لنظام الملفات

ستشرح هذه المقالة كيفية إجراء معالجة الملفات الأساسية من خلال لغة Go.

الفتح

طريقتان شائعتان لفتح الملفات هما استخدام دالتين توفرهما حزمة os، دالة Open تُرجع مؤشر ملف وخطأ،

go
func Open(name string) (*File, error)

أما OpenFile فهي توفر تحكماً أكثر دقة، ودالة Open هي مجرد تغليف بسيط لدالة OpenFile.

go
func OpenFile(name string, flag int, perm FileMode) (*File, error)

لنبدأ بالطريقة الأولى، يكفي توفير اسم الملف مباشرة، الكود كالتالي:

go
func main() {
   file, err := os.Open("README.txt")
   fmt.Println(file, err)
}

مسار البحث عن الملف يكون افتراضياً في مسار ملف go.mod للمشروع، وبما أن الملف README.txt غير موجود في المشروع، فستُرجع خطأ بشكل طبيعي.

<nil> open README.txt: The system cannot find the file specified.

لأن هناك أنواع كثيرة من أخطاء IO، يجب الحكم يدوياً على ما إذا كان الملف موجوداً، ولهذا توفر حزمة os أيضاً دوال ملائمة، الكود المعدل كالتالي:

go
func main() {
  file, err := os.Open("README.txt")
  if os.IsNotExist(err) {
    fmt.Println("الملف غير موجود")
  } else if err != nil {
    fmt.Println("خطأ في الوصول للملف")
  } else {
    fmt.Println("تم قراءة الملف بنجاح", file)
  }
}

عند التشغيل مرة أخرى:

الملف غير موجود

في الواقع، الدالة الأولى تقرأ الملف للقراءة فقط ولا يمكن تعديله:

go
func Open(name string) (*File, error) {
  return OpenFile(name, O_RDONLY, 0)
}

من خلال دالة OpenFile يمكن التحكم في مزيد من التفاصيل، مثل تعديل واصف الملف وأذونات الملف. حول واصف الملف، توفر حزمة os الثوابت التالية للاستخدام.

go
const (
   // القراءة فقط، الكتابة فقط، القراءة والكتابة يجب تحديد واحد من الثلاثة
   O_RDONLY int = syscall.O_RDONLY // فتح الملف في وضع القراءة فقط
   O_WRONLY int = syscall.O_WRONLY // فتح الملف في وضع الكتابة فقط
   O_RDWR   int = syscall.O_RDWR   // فتح الملف في وضع القراءة والكتابة
   // القيم المتبقية للتحكم في السلوك
   O_APPEND int = syscall.O_APPEND // عند الكتابة في الملف، إضافة البيانات إلى نهاية الملف
   O_CREATE int = syscall.O_CREAT  // إذا لم يكن الملف موجوداً إنشاؤه
   O_EXCL   int = syscall.O_EXCL   // استخدام مع O_CREATE، يجب ألا يكون الملف موجوداً
   O_SYNC   int = syscall.O_SYNC   // فتح الملف بطريقة IO متزامنة
   O_TRUNC  int = syscall.O_TRUNC  // اقتطاع الملف القابل للكتابة عند الفتح
)

حول أذونات الملف، توفر الثوابت التالية:

go
const (
   ModeDir        = fs.ModeDir        // d: دليل
   ModeAppend     = fs.ModeAppend     // a: إلحاق فقط
   ModeExclusive  = fs.ModeExclusive  // l: حصري
   ModeTemporary  = fs.ModeTemporary  // T: ملف مؤقت
   ModeSymlink    = fs.ModeSymlink    // L: رابط رمزي
   ModeDevice     = fs.ModeDevice     // D: ملف جهاز
   ModeNamedPipe  = fs.ModeNamedPipe  // p: أنبوب مسمى (FIFO)
   ModeSocket     = fs.ModeSocket     // S: مقبس نطاق Unix
   ModeSetuid     = fs.ModeSetuid     // u: setuid
   ModeSetgid     = fs.ModeSetgid     // g: setgid
   ModeCharDevice = fs.ModeCharDevice // c: جهاز حرف Unix، بشرط تعيين ModeDevice
   ModeSticky     = fs.ModeSticky     // t: بت لاصق
   ModeIrregular  = fs.ModeIrregular  // ?: ملف غير منتظم

   // قناع بتات النوع. للملفات العادية، لا يتم تعيين أي شيء.
   ModeType = fs.ModeType

   ModePerm = fs.ModePerm // بتات أذونات Unix، 0o777
)

فيما يلي مثال على كود لفتح ملف في وضع القراءة والكتابة، بصلاحيات 0666، مما يعني أن الجميع يمكنهم القراءة والكتابة على هذا الملف، وسيُنشأ تلقائياً إذا لم يكن موجوداً.

go
func main() {
  file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
  if os.IsNotExist(err) {
    fmt.Println("الملف غير موجود")
  } else if err != nil {
    fmt.Println("خطأ في الوصول للملف")
  } else {
    fmt.Println("تم فتح الملف بنجاح", file.Name())
    file.Close()
  }
}

الإخراج:

تم فتح الملف بنجاح README.txt

إذا كنت تريد فقط الحصول على بعض المعلومات عن الملف دون قراءته، يمكنك استخدام دالة os.Stat()، مثال:

go
func main() {
  fileInfo, err := os.Stat("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(fmt.Sprintf("%+v", fileInfo))
  }
}

الإخراج:

&{name:README.txt FileAttributes:32 CreationTime:{LowDateTime:3603459389 HighDateTime:31016791} LastAccessTime:{LowDateTime:3603459389 HighDateTime:31016791} LastWriteTime:{LowDateTime:3603459389 HighDateTime:31016791} FileSizeHigh
:0 FileSizeLow:0 Reserved0:0 filetype:0 Mutex:{state:0 sema:0} path:README.txt vol:0 idxhi:0 idxlo:0 appendNameToPath:false}

WARNING

بعد فتح ملف، تذكر دائماً إغلاقه، عادةً ما توضع عملية الإغلاق في جملة defer:

go
defer file.Close()

القراءة

عند نجاح فتح الملف، يمكن إجراء عملية القراءة. لعمليات قراءة الملفات، يوفر النوع *os.File الدوال المكشوفة التالية:

go
// قراءة الملف في شريحة البايت المُمررة
func (f *File) Read(b []byte) (n int, err error)

// مقارنة بالأولى، يمكن القراءة من إزاحة محددة
func (f *File) ReadAt(b []byte, off int64) (n int, err error)

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

go
func ReadFile(file *os.File) ([]byte, error) {
  buffer := make([]byte, 0, 512)
  for {
    // عندما لا تكون السعة كافية
    if len(buffer) == cap(buffer) {
      // التوسيع
      buffer = append(buffer, 0)[:len(buffer)]
    }
    // متابعة قراءة الملف
    offset, err := file.Read(buffer[len(buffer):cap(buffer)])
    // إضافة البيانات المكتوبة إلى الشريحة
    buffer = buffer[:len(buffer)+offset]
    // عند حدوث خطأ
    if err != nil {
      if errors.Is(err, io.EOF) {
        err = nil
      }
      return buffer, err
    }
  }
}

المنطق المتبقي كالتالي:

go
func main() {
   file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
   if err != nil {
      fmt.Println("خطأ في الوصول للملف")
   } else {
      fmt.Println("تم فتح الملف بنجاح", file.Name())
      bytes, err := ReadFile(file)
      if err != nil {
         fmt.Println("خطأ في قراءة الملف", err)
      } else {
         fmt.Println(string(bytes))
      }
      file.Close()
   }
}

الإخراج:

تم فتح الملف بنجاح README.txt
hello world!

بالإضافة إلى ذلك، يمكن استخدام دالتين ملاءمتين لقراءة الملفات، وهما دالة ReadFile من حزمة os، ودالة ReadAll من حزمة io. بالنسبة لـ os.ReadFile، يكفي توفير مسار الملف، أما io.ReadAll فتحتاج إلى توفير تنفيذ للنوع io.Reader.

os.ReadFile

go
func ReadFile(name string) ([]byte, error)

مثال الاستخدام:

go
func main() {
  bytes, err := os.ReadFile("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(string(bytes))
  }
}

الإخراج:

hello world!

io.ReadAll

go
func ReadAll(r Reader) ([]byte, error)

مثال الاستخدام:

go
func main() {

   file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
   if err != nil {
      fmt.Println("خطأ في الوصول للملف")
   } else {
      fmt.Println("تم فتح الملف بنجاح", file.Name())
      bytes, err := io.ReadAll(file)
      if err != nil {
         fmt.Println(err)
      } else {
         fmt.Println(string(bytes))
      }
      file.Close()
   }
}
تم فتح الملف بنجاح README.txt
hello world!

الكتابة

يوفر الهيكل os.File الدوال التالية للكتابة:

go
// كتابة شريحة بايت
func (f *File) Write(b []byte) (n int, err error)

// كتابة سلسلة نصية
func (f *File) WriteString(s string) (n int, err error)

// الكتابة من موقع محدد، عند الفتح في وضع os.O_APPEND، ستعيد خطأ
func (f *File) WriteAt(b []byte, off int64) (n int, err error)

إذا كنت تريد الكتابة إلى ملف، يجب فتحه في وضع O_WRONLY أو O_RDWR، وإلا لن تنجح الكتابة. فيما يلي مثال لفتح ملف في وضع os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC بصلاحيات 0666 للكتابة:

go
func main() {
  file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
  if err != nil {
    fmt.Println("خطأ في الوصول للملف")
  } else {
    fmt.Println("تم فتح الملف بنجاح", file.Name())
    for i := 0; i < 5; i++ {
      offset, err := file.WriteString("hello world!\n")
      if err != nil {
        fmt.Println(offset, err)
      }
    }
    fmt.Println(file.Close())
  }
}

بما أن الملف فُتح في وضع os.O_APPEND، فعند الكتابة ستُضاف البيانات إلى نهاية الملف، بعد التنفيذ يكون محتوى الملف كالتالي:

txt
hello world!
hello world!
hello world!
hello world!
hello world!

الكتابة بشريحة بايت هي عملية مشابهة، لن نكررها. لعمليات كتابة الملفات، توفر المكتبة القياسية أيضاً دوال ملائمة، وهي os.WriteFile و io.WriteString.

os.WriteFile

go
func WriteFile(name string, data []byte, perm FileMode) error

مثال الاستخدام:

go
func main() {
  err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
  if err != nil {
    fmt.Println(err)
  }
}

في هذه الحالة يكون محتوى الملف:

txt
hello world!

io.WriteString

go
func WriteString(w Writer, s string) (n int, err error)

مثال الاستخدام:

go
func main() {
   file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
   if err != nil {
      fmt.Println("خطأ في الوصول للملف")
   } else {
      fmt.Println("تم فتح الملف بنجاح", file.Name())
      for i := 0; i < 5; i++ {
         offset, err := io.WriteString(file, "hello world!\n")
         if err != nil {
            fmt.Println(offset, err)
         }
      }
      fmt.Println(file.Close())
   }
}
hello world!
hello world!
hello world!
hello world!
hello world!

دالة os.Create تُستخدم لإنشاء ملف، وهي في الأساس تغليف لـ OpenFile.

go
func Create(name string) (*File, error) {
   return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

WARNING

عند إنشاء ملف، إذا كان الدليل الأب غير موجود، سيفشل الإنشاء وسيُرجع خطأ.

النسخ

لنسخ الملفات، يجب فتح ملفين في نفس الوقت. الطريقة الأولى هي قراءة البيانات من الملف الأصلي ثم كتابتها في الملف الهدف، مثال:

go
func main() {
    // قراءة البيانات من الملف الأصلي
  data, err := os.ReadFile("README.txt")
  if err != nil {
    fmt.Println(err)
    return
  }
    // الكتابة في الملف الهدف
  err = os.WriteFile("README(1).txt", data, 0666)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("تم النسخ بنجاح")
  }
}

*os.File.ReadFrom

طريقة أخرى هي استخدام الدالة ReadFrom التي يوفرها os.File، عند فتح الملفات، واحد للقراءة فقط والآخر للكتابة فقط.

go
func (f *File) ReadFrom(r io.Reader) (n int64, err error)

مثال الاستخدام:

go
func main() {
  // فتح الملف الأصلي للقراءة فقط
  origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer origin.Close()
  // فتح ملف النسخة للكتابة فقط
  target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer target.Close()
  // قراءة البيانات من الملف الأصلي ثم كتابتها في ملف النسخة
  offset, err := target.ReadFrom(origin)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("تم نسخ الملف بنجاح", offset)
}

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

io.Copy

طريقة أخرى هي استخدام دالة io.Copy، فهي تقرأ وتكتب في نفس الوقت، تقرأ المحتوى أولاً في منطقة عازلة ثم تكتبه في الملف الهدف، والحجم الافتراضي للمنطقة العازلة هو 32 كيلوبايت.

go
func Copy(dst Writer, src Reader) (written int64, err error)

مثال الاستخدام:

go
func main() {
  // فتح الملف الأصلي للقراءة فقط
  origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer origin.Close()
  // فتح ملف النسخة للكتابة فقط
  target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer target.Close()
  // النسخ
  written, err := io.Copy(target, origin)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(written)
  }
}

يمكنك أيضاً استخدام io.CopyBuffer لتحديد حجم المنطقة العازلة.

إعادة التسمية

إعادة التسمية يمكن فهمها أيضاً على أنها نقل الملف، ستُستخدم دالة Rename من حزمة os.

go
func Rename(oldpath, newpath string) error

مثال:

go
func main() {
  err := os.Rename("README.txt", "readme.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("تم إعادة التسمية بنجاح")
  }
}

هذه الدالة تعمل بنفس الطريقة مع المجلدات.

الحذف

عملية الحذف أبسط من العمليات الأخرى، ستُستخدم دالتان فقط من حزمة os:

go
// حذف ملف واحد أو دليل فارغ، عندما لا يكون الدليل فارغاً تُرجع خطأ
func Remove(name string) error

// حذف جميع الملفات والأدلة في الدليل المحدد بما في ذلك الأدلة الفرعية والملفات الفرعية
func RemoveAll(path string) error

الاستخدام بسيط جداً، فيما يلي مثال لحذف دليل:

go
func main() {
  // حذف جميع الملفات والأدلة الفرعية في الدليل الحالي
  err := os.RemoveAll(".")
  if err != nil {
    fmt.Println(err)
  }else {
    fmt.Println("تم الحذف بنجاح")
  }
}

فيما يلي مثال لحذف ملف واحد:

go
func main() {
  // حذف جميع الملفات والأدلة الفرعية في الدليل الحالي
  err := os.Remove("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("تم الحذف بنجاح")
  }
}

التحديث

الدالة os.Sync تغلف استدعاء النظام الأساسي Fsync، المستخدم لتنفيذ عمليات IO المخزنة مؤقتاً في نظام التشغيل على القرص:

go
func main() {
  create, err := os.Create("test.txt")
  if err != nil {
    panic(err)
  }
  defer create.Close()

  _, err = create.Write([]byte("hello"))
  if err != nil {
    panic(err)
  }

    // التحديث على القرص
  if err := create.Sync();err != nil {
    return
  }
}

المجلدات

العديد من عمليات المجلدات مشابهة لعمليات الملفات

القراءة

لفتح المجلدات طريقتان:

os.ReadDir

الطريقة الأولى هي استخدام دالة os.ReadDir:

go
func ReadDir(name string) ([]DirEntry, error)
go
func main() {
   // الدليل الحالي
   dir, err := os.ReadDir(".")
   if err != nil {
      fmt.Println(err)
   } else {
      for _, entry := range dir {
         fmt.Println(entry.Name())
      }
   }
}

*os.File.ReadDir

الطريقة الثانية هي استخدام دالة *os.File.ReadDir، ودالة os.ReadDir في الأساس تغليف بسيط لـ *os.File.ReadDir.

go
// عندما n < 0، تقرأ جميع محتويات المجلد
func (f *File) ReadDir(n int) ([]DirEntry, error)
go
func main() {
   // الدليل الحالي
   dir, err := os.Open(".")
   if err != nil {
      fmt.Println(err)
   }
   defer dir.Close()
   dirs, err := dir.ReadDir(-1)
   if err != nil {
      fmt.Println(err)
   } else {
      for _, entry := range dirs {
         fmt.Println(entry.Name())
      }
   }
}

الإنشاء

لإنشاء المجلدات ستُستخدم دالتان من حزمة os:

go
// إنشاء دليل بالاسم المحدد وبالصلاحيات المحددة
func Mkdir(name string, perm FileMode) error

// مقارنة بالأولى، هذه الدالة تنشئ جميع الأدلة الأب الضرورية
func MkdirAll(path string, perm FileMode) error

مثال:

go
func main() {
  err := os.Mkdir("src", 0666)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("تم الإنشاء بنجاح")
  }
}

النسخ

يمكننا كتابة دالة لاجتياز المجلد بأكمله بشكل تكراري، لكن المكتبة القياسية filepath توفر بالفعل دالة مشابهة، لذا يمكن استخدامها مباشرة. فيما يلي مثال بسيط لنسخ مجلد:

go
func CopyDir(src, dst string) error {
    // التحقق من حالة المجلد المصدر
  _, err := os.Stat(src)
  if err != nil {
    return err
  }

  return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
    if err != nil {
      return err
    }

        // حساب المسار النسبي
    rel, err := filepath.Rel(src, path)
    if err != nil {
      return err
    }

        // ربط المسار الهدف
    destpath := filepath.Join(dst, rel)

        // إنشاء المجلد
    var dirpath string
    var mode os.FileMode = 0755
    if info.IsDir() {
      dirpath = destpath
      mode = info.Mode()
    } else if info.Mode().IsRegular() {
      dirpath = filepath.Dir(destpath)
    }

    if err := os.MkdirAll(dirpath, mode); err != nil {
      return err
    }

        // إنشاء الملف
    if info.Mode().IsRegular() {
      srcfile, err := os.Open(path)
      if err != nil {
        return err
      }
            // تذكر إغلاق الملف دائماً
      defer srcfile.Close()
      destfile, err := os.OpenFile(destpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
      if err != nil {
        return err
      }
      defer destfile.Close()

            // نسخ محتوى الملف
      if _, err := io.Copy(destfile, srcfile); err != nil {
        return err
      }
      return nil
    }

    return nil
  })
}

دالة filepath.Walk تجتاز المجلد بأكمله بشكل تكراري، خلال العملية، عند مواجهة مجلد يتم إنشاء مجلد، وعند مواجهة ملف يتم إنشاء ملف جديد ونسخه، الكود أكثر قليلاً من نسخ الملفات لكنه ليس معقداً.

Golang تم تحريره بواسطة www.golangdev.cn