كيفية تنظيم مشروع Go بعدة ملفات: تحت الغطاء

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مشروع Go من ملف واحد - هذا سهل. وماذا عن عشرة ملفات؟

\n\n

تخيل: أنت تكتب أداة بلغة Go لتحليل السجلات. في البداية، كل شيء يتسع في ملف واحد main.go. يعمل، كل شيء رائع. ولكن بعد أسبوع، يتضخم الكود: تظهر دوال لقراءة الملفات، للتصفية، لإرسال البيانات إلى قاعدة البيانات. ملف واحد يتحول إلى وحش من 2000 سطر. هل هذا مألوف؟

\n\n

هنا يطرح السؤال الذي يقلق كل مبتدئ ثانٍ (والذي حصد آلاف المشاهدات على Stack Overflow): كيف تنظم مشروع Go بعدة ملفات بشكل صحيح؟ ليس فقط توزيع الكود في المجلدات، بل جعله يعمل، ويُترجم، ولا يسبب أخطاء مثل undefined أو import cycle not allowed.

\n\n

في هذه المقالة، لن نقدم قالبًا فقط. سننظر تحت غطاء مترجم Go ونفهم كيف يعمل الربط بين الملفات والحزم والوحدات النمطية. ستتعرف على لماذا go run *.go أحيانًا ينقذ الموقف وأحيانًا لا، وكيف تبني بنية قابلة للتوسع.

\n\n

الملف، الحزمة، الوحدة النمطية - التسلسل الهرمي الأساسي

\n\n

قبل الاندفاع إلى الكود، دعنا نحلل ثلاثة مفاهيم رئيسية. بدونها، يتحول تنظيم مشروع Go إلى تخمين.

\n\n
    \n
  • الملف (.go) - هو مجرد ملف نصي يحتوي على كود. بذاته لا يعني شيئًا للمترجم.
  • \n
  • الحزمة (package) - هي مجموعة منطقية من الملفات الموجودة في دليل واحد. جميع الملفات في المجلد ملزمة بأن يكون لها نفس تعريف package. إذا كانت الملفات في المجلد تحتوي على حزم مختلفة، سيصدر المترجم خطأ.
  • \n
  • الوحدة النمطية (go.mod) - هي جذر مشروعك. تحدد اسم الوحدة النمطية، تبعياتها، وإصدارات Go. يمكن أن تحتوي الوحدة النمطية على العديد من الحزم.
  • \n
\n\n

القاعدة الذهبية: دليل واحد = حزمة واحدة. الاستثناء هو الاختبارات، ولكن عنها لاحقًا.

\n\n

مثال عملي: مشروع "محلل السجلات"

\n\n

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

\n\n

المرحلة 1: كل شيء في ملف واحد (سيء)

\n\n
// main.go\npackage main\n\nimport (\n    "bufio"\n    "fmt"\n    "os"\n    "strings"\n)\n\nfunc readLog(path string) ([]string, error) {\n    file, err := os.Open(path)\n    if err != nil {\n        return nil, err\n    }\n    defer file.Close()\n\n    var lines []string\n    scanner := bufio.NewScanner(file)\n    for scanner.Scan() {\n        lines = append(lines, scanner.Text())\n    }\n    return lines, scanner.Err()\n}\n\nfunc filterLines(lines []string, keyword string) []string {\n    var result []string\n    for _, line := range lines {\n        if strings.Contains(line, keyword) {\n            result = append(result, line)\n        }\n    }\n    return result\n}\n\nfunc main() {\n    lines, err := readLog("server.log")\n    if err != nil {\n        fmt.Println("خطأ:", err)\n        return\n    }\n    filtered := filterLines(lines, "ERROR")\n    for _, l := range filtered {\n        fmt.Println(l)\n    }\n}\n
\n\n

يعمل. لكن تخيل أن readLog يصبح أكثر تعقيدًا (بحاجة لدعم gzip، الترميزات)، و filterLines يزداد بالتعبيرات النمطية. سيصبح الملف غير قابل للقراءة.

\n\n

المرحلة 2: التقسيم إلى حزم (صحيح)

\n\n

لنقم بإنشاء هيكل الدليل:

\n\n
log-analyzer/\n├── go.mod\n├── main.go\n└── pkg/\n    └── logreader/\n        ├── reader.go\n        └── filter.go\n
\n\n

نقوم بتهيئة الوحدة النمطية:

\n\n
go mod init log-analyzer
\n\n

الآن نكتب كود الحزمة logreader. لاحظ: جميع الملفات في مجلد pkg/logreader/ لها package logreader.

\n\n

pkg/logreader/reader.go

\n\n
package logread                

المدونات

توصيات الكتب