Menu
flag Ar iconالعربيةdown icon

هيكلة مشروع Golang: cmd وinternal وتنظيم الحزم

كيف تنظّم مشروع Go: ابدأ مسطّحًا، وقسّم إلى حزم عندما يوجد سبب، واستخدم cmd/ لعدة ملفات تنفيذية وinternal/ للشيفرة التي لا يجوز لغيرك استيرادها، وسمِّ الحزم جيدًا، وأبقِ الاختبارات بجوار الشيفرة.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

ابدأ بملف واحد

يمكن لبرنامج Go أن يعيش كاملًا في main.go واحد، ويجب أن تفعل الأدوات الصغيرة ذلك. هذا البرنامج الكامل يحلّل المدخلات ويؤدّي عمله ويطبع تقريرًا، في حزمة واحدة:

عندما يكبر، قسّمه إلى ملفات أكثر في المجلد نفسه والحزمة نفسها (parse.go وreport.go وmain.go). ملفات الحزمة الواحدة تتشارك كل المعرّفات، فلا حاجة لتصدير أي شيء أو استيراده. وغالبًا تكون هذه كل الهيكلة التي يحتاجها مشروع من بضعة آلاف من الأسطر.

تذكّر أن package main متعدّدة الملفات يجب تشغيلها كحزمة: go run .، لا go run main.go. وإلا تُترجم الملفات منفردة وتحصل على أخطاء undefined للأسماء المعرّفة في الملفات الأخرى.

لا يوجد تخطيط إلزامي

لا تفرض Go أي هيكل مجلدات سوى "حزمة واحدة لكل مجلد". الإرشاد الرسمي هو صفحة Organizing a Go module على go.dev، وهي تصف بضعة أشكال، لا قواعد.

المستودع golang-standards/project-layout على GitHub يُنسخ على نطاق واسع ويُظنّ خطأ أنه معيار. إنه مجموعة مجتمعية من أعراف المشاريع الكبيرة، وقد فتح Russ Cox، قائد Go التقني آنذاك، تذكرة فيه تنصّ على أنه ليس معيارًا من Go. معظم مجلداته (pkg/ وapi/ وbuild/ وdeployments/) لا معنى لها إلا في قواعد الشيفرة الكبيرة. نسخ الهيكل كاملًا إلى مشروع جديد يصنع مجلدات فارغة ومسارات استيراد عميقة بلا فائدة.

القواعد التي تفرضها الأداة قصيرة:

  • المجلد الواحد حزمة واحدة. كل ملفات .go فيه (باستثناء ملفات _test.go التي تستخدم اللاحقة _test) تتشارك اسم حزمة.
  • مسار الاستيراد هو مسار الوحدة مع مسار المجلد.
  • المجلد المسمّى internal يقيّد من يستطيع استيراد ما بداخله.
  • تتجاهل أداة go المجلدات المسمّاة testdata، والتي تبدأ بـ . أو _.

الأشكال الشائعة

أمر واحد

todo/
├── go.mod        module github.com/you/todo
├── main.go
├── parse.go
├── report.go
└── parse_test.go

مسطّح، وكل شيء في package main. يعمل go install github.com/you/todo@latest ويُسمّى الملف التنفيذي todo.

مكتبة

slug/
├── go.mod        module github.com/you/slug
├── slug.go       package slug
├── slug_test.go
├── example_test.go
└── internal/
    └── table/    helpers the public package uses, hidden from users

تقع الحزمة في جذر الوحدة، فيستورد المستخدمون github.com/you/slug ويستدعون slug.Make(...). كل ما لا تريد دعمه كواجهة برمجية عامة يوضع تحت internal/.

خدمة أو عدة ملفات تنفيذية

shop/
├── go.mod                module github.com/you/shop
├── cmd/
│   ├── shop-api/
│   │   └── main.go       package main: flags, config, wiring
│   └── shop-worker/
│       └── main.go
├── internal/
│   ├── order/            package order: domain types and logic
│   │   ├── order.go
│   │   └── order_test.go
│   ├── store/            package store: database access
│   └── httpapi/          package httpapi: handlers, routing
├── migrations/
└── README.md

يحمل cmd/<name>/main.go مجلدًا لكل ملف تنفيذي، ويصبح اسم المجلد اسم الملف التنفيذي (ينتج go build ./cmd/shop-api الملف shop-api). تبقى كل main رفيعة: تقرأ الإعدادات، وتبني الاعتماديات، وتشغّل الخادم. والشيفرة الحقيقية تعيش في حزم تحت internal/، حيث يستطيع الملفان التنفيذيان استخدامها ولا تستطيع أي وحدة أخرى.

هذا هو التخطيط الذي تنتهي إليه معظم خدمات Go. لجأ إليه عندما يصير لديك ملف تنفيذي ثانٍ أو سبب حقيقي لتقسيم الحزم، لا في اليوم الأول.

الأمر go يفرض internal/

لا يمكن استيراد حزمة تحت internal/ إلا من الشيفرة في الشجرة التي جذرها أب internal. ومن وحدة أخرى يفشل البناء:

package example.com/b
	main.go:6:2: use of internal package example.com/a/internal/secret not allowed

هذا يجعل internal/ الأداة لإبقاء سطح واجهتك البرمجية صغيرًا. الشيفرة هناك تتغيّر بحرية، لأنك تعرف أن كل مستدعٍ في مستودعك. في التطبيقات، وضع كل شيء تقريبًا في internal/ معقول. وفي المكتبات، يفصل ما تعد بإبقائه مستقرًا عمّا لا تعد به.

يعمل internal في أي عمق: shop/internal/order مرئي لكل shop/، بينما shop/internal/order/internal/pricing مرئي فقط داخل shop/internal/order/.

تسمية الحزم

اسم الحزمة جزء من كل موضع استدعاء، فهو أهم من شجرة المجلدات.

  • قصير، بحروف صغيرة، كلمة واحدة: order وstore وhttpapi. لا شرطات سفلية ولا حروف كبيرة في الوسط.
  • سمّه بما يقدّمه، لا بما يحتويه. util وcommon وhelpers وmisc وmodels لا تقول شيئًا وتصير مكبّات؛ وإرشادات أسلوب فريق Go لا تنصح بها صراحة. ضع الدالة المساعدة في الحزمة التي تستخدمها، أو في حزمة مسمّاة بغرضها (slug وretry).
  • تجنّب التكرار. يكتب المستدعون order.Order وorder.New، فلا تسمِّ الأشياء order.OrderService أو order.NewOrder. المكتبة القياسية تفعل هذا باتّساق: http.Server لا http.HTTPServer.
  • يجب أن يتطابق اسم المجلد واسم الحزمة، باستثناء package main في مجلدات الأوامر. عندما يختلفان يضطر القرّاء إلى فتح ملف ليعرفوا أي معرّف يُدخله الاستيراد.

قسّم الحزم حسب المسؤولية لا حسب النوع. تقسيم models/ وcontrollers/ وservices/ (الشائع في منظومات أخرى) يجبر كل ميزة على لمس ثلاث حزم ويميل إلى صنع حلقات استيراد، وهي ما تمنعه Go. الحزم المنظّمة حول مفهوم من المجال (order وpayment وuser) تحمل كل منها أنواعها ومنطقها معًا.

أين توضع الاختبارات

تعيش الاختبارات بجوار الشيفرة في المجلد نفسه، في ملفات بأسماء *_test.go. لا توجد شجرة tests/.

  • package order في order_test.go: اختبار داخلي، يستطيع استخدام المعرّفات غير المُصدَّرة.
  • package order_test في المجلد نفسه: اختبار خارجي، لا يرى إلا الواجهة البرمجية المُصدَّرة، كما يراها المستدعي. مفيد للأمثلة ولتجنّب حلقات الاستيراد في الاختبارات.
  • توضع ملفات البيانات الثابتة في مجلد testdata/ بجوار الاختبارات. تتجاهله أداة go، وتعمل الاختبارات ومجلد عملها هو مجلد الحزمة، فتعمل المسارات النسبية مثل testdata/big.json.

ملفات أخرى في الجذر

الملف أو المجلدالغرض
go.mod، go.sumتعريف الوحدة وبصمات الاعتماديات، دائمًا في الجذر
README.md، LICENSEكما في أي مشروع
Makefile أو Taskfile.ymlاختصارات بناء اختيارية
Dockerfileعادة في الجذر للخدمات
migrations/، web/، docs/ملفات ليست Go، مسمّاة بما تحمله
tools.goالطريقة الأقدم لتثبيت اعتماديات الأدوات؛ تستبدلها Go 1.24 بأسطر tool في go.mod (go get -tool)
go.workمساحة عمل لتطوير عدة وحدات معًا محليًا؛ لا يُرفع عادة في مشاريع الوحدة الواحدة

أخطاء شائعة

  • نسخ قالب كبير لمشروع صغير. ابدأ مسطّحًا وأضف المجلدات عندما يظهر ملف تنفيذي ثانٍ أو حدّ حقيقي.
  • حزم باسم utils أو common. سمِّ الحزم بما تفعله.
  • حزمة لكل ملف أو لكل نوع. حزم Go وحدات أكبر من أصناف Java. الحزمة ذات العشرة ملفات أمر عادي.
  • حلقات الاستيراد. لا تستطيع حزمتان استيراد إحداهما الأخرى. يعني ذلك عادة أنهما تنتميان معًا، أو أن نوعًا مشتركًا يجب نقله إلى حزمة أدنى مستوى، أو أن أحد الطرفين يجب أن يعتمد على واجهة صغيرة بدل الحزمة الأخرى.
  • pkg/ بحكم العادة. يضيف جزءًا إلى كل مسار استيراد دون أن يضيف معنى.
  • الاختبارات في مجلد منفصل. لا ترى الشيفرة غير المُصدَّرة هناك، ولا تتوقّعها الأدوات.

الأسئلة الشائعة

هل يوجد تخطيط رسمي لمشاريع Go؟

ليس تخطيطًا إلزاميًا. ينشر فريق Go إرشادًا في go.dev/doc/modules/layout يصف بضعة أشكال شائعة (حزمة واحدة، أمر واحد، عدة أوامر مع internal/). المستودع الشهير golang-standards/project-layout مشروع مجتمعي لا معيار من Go، وقد قال فريق Go ذلك علنًا.

ما هو المجلد internal في Go؟

الحزمة التي يحتوي مسار استيرادها على العنصر internal لا يمكن استيرادها إلا من شيفرة جذرها أب ذلك المجلد internal. يمكن استيراد example.com/app/internal/store من أي مكان تحت example.com/app/، ويرفض الأمر go الاستيراد من أي وحدة أخرى برسالة use of internal package ... not allowed.

هل أستخدم المجلد pkg في Go؟

هو اختياري ويضيف جزءًا إلى المسار دون أن يضيف معنى. بعض المشاريع الكبيرة تستخدم pkg/ لفصل شيفرة المكتبة العامة عن البقية، لكن المكتبة القياسية لـ Go ومعظم المشاريع الحديثة لا تفعل ذلك. ضع الحزم القابلة للاستيراد في جذر الوحدة أو في مجلدات مسمّاة، وكل ما هو خاص في internal/.

أين توضع ملفات الاختبار في مشروع Go؟

في المجلد نفسه مع الشيفرة التي تختبرها، بأسماء *_test.go. لا تملك Go شجرة اختبارات منفصلة. وتوضع ملفات البيانات الثابتة في مجلد testdata بجوار الاختبارات، وتتجاهله أداة go عند البحث عن الحزم.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن