مشروع Go من ملف واحد - هذا سهل. وماذا عن عشرة ملفات؟
\n\nتخيل: أنت تكتب أداة بلغة Go لتحليل السجلات. في البداية، كل شيء يتسع في ملف واحد main.go. يعمل، كل شيء رائع. ولكن بعد أسبوع، يتضخم الكود: تظهر دوال لقراءة الملفات، للتصفية، لإرسال البيانات إلى قاعدة البيانات. ملف واحد يتحول إلى وحش من 2000 سطر. هل هذا مألوف؟
هنا يطرح السؤال الذي يقلق كل مبتدئ ثانٍ (والذي حصد آلاف المشاهدات على Stack Overflow): كيف تنظم مشروع Go بعدة ملفات بشكل صحيح؟ ليس فقط توزيع الكود في المجلدات، بل جعله يعمل، ويُترجم، ولا يسبب أخطاء مثل undefined أو import cycle not allowed.
في هذه المقالة، لن نقدم قالبًا فقط. سننظر تحت غطاء مترجم Go ونفهم كيف يعمل الربط بين الملفات والحزم والوحدات النمطية. ستتعرف على لماذا go run *.go أحيانًا ينقذ الموقف وأحيانًا لا، وكيف تبني بنية قابلة للتوسع.
الملف، الحزمة، الوحدة النمطية - التسلسل الهرمي الأساسي
\n\nقبل الاندفاع إلى الكود، دعنا نحلل ثلاثة مفاهيم رئيسية. بدونها، يتحول تنظيم مشروع Go إلى تخمين.
\n\n- \n
- الملف (
.go) - هو مجرد ملف نصي يحتوي على كود. بذاته لا يعني شيئًا للمترجم. \n - الحزمة (
package) - هي مجموعة منطقية من الملفات الموجودة في دليل واحد. جميع الملفات في المجلد ملزمة بأن يكون لها نفس تعريفpackage. إذا كانت الملفات في المجلد تحتوي على حزم مختلفة، سيصدر المترجم خطأ. \n - الوحدة النمطية (
go.mod) - هي جذر مشروعك. تحدد اسم الوحدة النمطية، تبعياتها، وإصدارات Go. يمكن أن تحتوي الوحدة النمطية على العديد من الحزم. \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 يزداد بالتعبيرات النمطية. سيصبح الملف غير قابل للقراءة.
المرحلة 2: التقسيم إلى حزم (صحيح)
\n\nلنقم بإنشاء هيكل الدليل:
\n\nlog-analyzer/\n├── go.mod\n├── main.go\n└── pkg/\n └── logreader/\n ├── reader.go\n └── filter.go\n\n\nنقوم بتهيئة الوحدة النمطية:
\n\ngo mod init log-analyzer\n\nالآن نكتب كود الحزمة logreader. لاحظ: جميع الملفات في مجلد pkg/logreader/ لها package logreader.
pkg/logreader/reader.go
\n\npackage logread