تحدد الكلمة المفتاحية package اسم الحزمة التي ينتمي إليها ملف مصدر Go، ويجب أن تكون أول سطر من الكود في كل ملف .go. تبني package main برنامجًا تنفيذيًا، أما أي اسم آخر مثل package billing فيبني مكتبة تستوردها حزم أخرى. ويجب أن تستخدم كل الملفات في المجلد الواحد اسم الحزمة نفسه، وهذا الاسم هو ما يكتبه الكود الآخر قبل النقطة للوصول إلى الأسماء المُصدَّرة من حزمتك، كما في billing.Total (مواصفات Go).
الخلاصة
- يبدأ كل ملف
.goبالسطرpackage name. لا يسبقه إلا التعليقات والأسطر الفارغة. package mainهي البرنامج نفسه. تحتاج إلىfunc main()بلا معاملات وبلا قيم مُعادة، ويحوّلهاgo buildإلى ملف تنفيذي. ولا يمكن استيرادها.- المجلد الواحد حزمة واحدة. إذا وُجدت في المجلد نفسه ملفات بأسماء حزم مختلفة، يفشل البناء بالخطأ
found packages billing (invoice.go) and payments (tax.go). والاستثناء الأساسي هو حزم_testفي ملفات الاختبار. - اسم الحزمة ومسار الاستيراد شيئان مختلفان. تستورد
"gopkg.in/yaml.v3"ثم تستدعيyaml.Unmarshal. والعُرف أن يطابق الاسمُ العنصرَ الأخير من المسار. - أسماء الحزم الجيدة كلمات مفردة قصيرة بأحرف صغيرة، بلا شرطات سفلية ولا mixedCaps. وتجنّب
utilوcommon. - الظهور (visibility) يُحدَّد على مستوى الحزمة. الاسم الذي يبدأ بحرف كبير يُصدَّر إلى الحزم الأخرى. أما الاسم الذي يبدأ بحرف صغير فيظهر لكل ملفات الحزمة نفسها ولا يظهر خارجها.
ماذا تفعل package main في Go؟
تخبر package main أدوات Go ببناء ملف تنفيذي بدلًا من مكتبة. والدالة main في هذه الحزمة هي نقطة بدء البرنامج، وعندما تعود ينتهي البرنامج. إليك خدمة HTTP كاملة في ملف واحد:
example.gogopackage main import ( "fmt" "log" "net/http" ) func main() { mux := http.NewServeMux() mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "ok") }) log.Fatal(http.ListenAndServe(":8080", mux)) }
يترجمها go run . ويشغّلها، ويكتب go build -o bin/api . ملفًا تنفيذيًا. وتفرض أدوات Go القاعدتين المتعلقتين بـ main كلتيهما. فالحزمة package main التي لا تحتوي على دالة main تفشل عند الربط:
example.texttextruntime.main_main·f: function main is undeclared in the main package
وتشغيل حزمة مكتبة بالأمر go run ./billing يفشل قبل أن تبدأ الترجمة أصلًا:
example.texttextpackage example.com/shop/billing is not a main package
ولأن main هي البرنامج نفسه، لا تستطيع أي حزمة أخرى استيرادها. ومحاولة ذلك تعطي الخطأ import "example.com/shop/cmd/api" is a program, not an importable package. لذلك تحتفظ مشاريع Go بمنطقها في حزم مكتبات وتُبقي main صغيرة. والمستودع الذي يضم عدة ملفات تنفيذية يضع كلًّا منها في مجلد خاص به، عادةً تحت cmd/، وكل مجلد من هذه المجلدات حزمة package main مستقلة. والواجهة الخلفية (backend) الخاصة بـ LevelUpGo نفسها تضم 11 منها، من خادم الـ API إلى مستورد المحتوى، وكلها تتشارك 22 حزمة مكتبة تحت internal/.
هل يمكن أن يحمل ملفان في مجلد واحد اسمَي حزمتين مختلفين؟
لا. يتعامل الأمر go مع المجلد على أنه حزمة واحدة، لذا يجب أن يصرّح كل ملف .go فيه عن الاسم نفسه. لنفترض أن billing/invoice.go يبدأ بـ package billing، ثم أضاف أحدهم billing/tax.go مع package payments. عندها يتوقف go build ./billing بالخطأ:
example.texttextfound packages billing (invoice.go) and payments (tax.go) in /home/dev/shop/billing
والحل أن تعيد تسمية إحدى العبارتين أو تنقل الملف إلى مجلد خاص به. والمجلد هو أيضًا أصغر وحدة يمكنك استيرادها، فإذا كان ملفان ينتميان إلى حزمتين مختلفتين، فهما يحتاجان إلى مجلدين مختلفين.
في المقابل، تقسيم الحزمة على عدة ملفات لا يكلّف شيئًا. فكل ملف في المجلد يرى أسماء الملفات الأخرى كلها، المُصدَّرة منها وغير المُصدَّرة، دون استيراد أي شيء. والحزمة billing المكوّنة من invoice.go و tax.go و refund.go تتصرف تمامًا كأنها ملف واحد طويل.
ما الفرق بين اسم الحزمة ومسار الاستيراد؟
مسار الاستيراد هو المكان الذي توجد فيه الحزمة، واسم الحزمة هو المعرّف الذي تستخدمه في الكود. يتكوّن مسار الاستيراد من مسار الوحدة (module) في go.mod مضافًا إليه المجلد، مثل example.com/shop/billing. أما اسم الحزمة فيأتي من عبارة package في الملفات داخل ذلك المجلد.
والعُرف أن يتطابقا، فيكون الاسم هو العنصر الأخير من المسار. يعرض الجدول مسارًا واحدًا يتطابق فيه الاثنان، والاستثناءين المعتادين، وكلاهما سببه الإصدارات:
| مسار الاستيراد | اسم الحزمة | سبب الاختلاف |
|---|---|---|
net/http | http | متطابقان |
gopkg.in/yaml.v3 | yaml | يحمل المسار لاحقة الإصدار .v3 |
math/rand/v2 | rand | مجلد الإصدار الرئيسي /v2 |
github.com/jackc/pgx/v5 | pgx | مجلد الإصدار الرئيسي /v5 |
عندما يتشارك استيرادان الاسم نفسه، تعيد تسمية أحدهما في كتلة الاستيراد. فالكود الذي ينشئ رموز الجلسات (session tokens) ويضيف أيضًا تذبذبًا عشوائيًا (jitter) لإعادة المحاولة يحتاج غالبًا إلى crypto/rand و math/rand/v2 معًا، وكلاهما اسمه rand:
example.gogopackage tokens import ( "crypto/rand" "encoding/hex" mrand "math/rand/v2" "time" ) // NewSessionID returns a random 32-character hex session ID. func NewSessionID() string { b := make([]byte, 16) rand.Read(b) return hex.EncodeToString(b) } // RetryDelay adds up to 100ms of jitter to a base delay. func RetryDelay(base time.Duration) time.Duration { return base + time.Duration(mrand.IntN(100))*time.Millisecond }
إعادة التسمية تسري داخل ذلك الملف فقط. أما بقية الملفات فتظل ترى الاسم الذي صرّحت به الحزمة. ويشرح مقال الكلمة المفتاحية import في Go الأسماء المستعارة والاستيراد الفارغ وبقية أشكال الاستيراد.
يمكن أن يحتوي مجلد على حزمة يختلف اسمها عن اسم المجلد، وستترجمها Go دون مشكلة. لكن الثمن يدفعه القارئ. فمن يستورد example.com/shop/httpapi-v2 مضطر إلى فتح الكود المصدري ليعرف أن اسم الحزمة httpapi. لذا اجعل الاسمين متطابقين ما لم تفرض لاحقة الإصدار اختلافهما.
كيف تسمّي حزمة في Go؟
تتبع أسماء الحزم في Go مجموعة قصيرة من القواعد مأخوذة من Effective Go ومن مقال Package names في مدونة Go:
- أحرف صغيرة وكلمة واحدة.
billingوhttpapiوratelimit. لاrate_limitولاrateLimit. تسمح بهما المواصفات لأن اسم الحزمة يمكن أن يكون أي معرّف، لكن كود Go الاصطلاحي لا يستخدمهما. - قصير ومحدد. يُكتب الاسم عند كل استدعاء. لذا
strconvأفضل منstringconversion، وauthأفضل منauthenticationservice. - مسمّى بحسب ما يقدّمه. الأسماء
utilوcommonوhelpersوmiscوmodelsلا تقول شيئًا عن محتواها، وتتضخم لتصبح حزمًا يستوردها كل شيء. قسّمها بحسب الغرض بدلًا من ذلك، مثلmoneyأوslugifyأوpagination. - لا تكرر اسم الحزمة في الأسماء المُصدَّرة، لأن المستدعين يكتبونه دائمًا.
http.Serverصحيح وhttp.HTTPServerليس كذلك. وbilling.Totalصحيح وbilling.BillingTotalليس كذلك. ودالة الإنشاء (constructor) للنوع الرئيسي في الحزمة تُسمّى في الغالبNewفقط، كما فيring.New. - ليس كلمة مفتاحية ولا
_. السطرpackage typeخطأ في الصياغة، وpackage _يفشل بالخطأinvalid package name _.
اقرأ الاسم مع المعرّفات المُصدَّرة منه، لأن هذه هي الطريقة التي يراه بها كل مستدعٍ. فالعبارتان ratelimit.New(100, time.Minute) و config.Load("app.yaml") تُقرآن كجملتين واضحتين. أما utils.NewRateLimiterUtil فلا.
كيف تتحكم package في الظهور (visibility) في Go؟
لا توجد في Go كلمات مفتاحية مثل public أو private أو protected. بدلًا من ذلك تكون الحزمة هي الحد الفاصل. الاسم الذي يبدأ بحرف كبير يكون مُصدَّرًا ويظهر لأي حزمة تستورده. أما الاسم الذي يبدأ بحرف صغير فيكون غير مُصدَّر ولا يظهر إلا داخل حزمته، في كل ملفاتها.
example.gogo// Package billing calculates invoice totals and applies VAT. package billing // VATPercent is the standard VAT rate applied to invoices. const VATPercent = 25 // Total returns the sum of line items in cents, including VAT. func Total(lineItems []int64) int64 { var sum int64 for _, c := range lineItems { sum += c } return addVAT(sum) } func addVAT(cents int64) int64 { return cents + cents*VATPercent/100 }
يستطيع معالج (handler) في package main استدعاء billing.Total، لكن billing.addVAT(1000) يفشل بالخطأ name addVAT not exported by package billing. والقاعدة نفسها تنطبق على حقول الـ struct والـ methods. وللسبب نفسه لا ترى encoding/json من الـ struct إلا حقوله المُصدَّرة.
أما الكود الذي تتشاركه عدة حزم من حزمك ولا ينبغي أن تستورده وحدات خارجية، فتوفّر له Go مجلدات internal. الحزمة الموجودة تحت example.com/shop/internal/pricing يمكن أن يستوردها أي شيء جذره example.com/shop، ولا شيء غيره. وأي وحدة أخرى تحاول ذلك تحصل على:
example.texttextuse of internal package example.com/shop/internal/pricing not allowed
يشرح مقال هيكلة مشاريع Go متى تلجأ إلى internal/ وكيف تنظّم cmd/ وحزم المكتبات في خدمة حقيقية.
ما حزمة _test في Go؟
يمكن لملفات الاختبار، أي الملفات التي تنتهي بـ _test.go، أن تستخدم أحد اسمين للحزمة. يضع package billing الاختبار داخل الحزمة، فيستطيع استدعاء الدوال غير المُصدَّرة:
example.gogopackage billing import "testing" func TestAddVAT(t *testing.T) { if got := addVAT(1000); got != 1250 { t.Fatalf("addVAT(1000) = %d, want 1250", got) } }
أما package billing_test فتجعله حزمة اختبار خارجية. تقع في المجلد نفسه، لكن عليها أن تستورد billing كأي مستدعٍ آخر، ولا تستطيع استخدام إلا الأسماء المُصدَّرة:
example.gogopackage billing_test import ( "testing" "example.com/shop/billing" ) func TestTotal(t *testing.T) { got := billing.Total([]int64{1000, 2000}) if got != 3750 { t.Fatalf("Total = %d, want 3750", got) } }
باستثناء الملفات التي يستبعدها قيد بناء (build constraint)، مثل سكربت توليد يحمل //go:build ignore، فإن اللاحقة _test هي الطريقة الوحيدة لوجود اسمَي حزمتين في مجلد واحد. ويبني go test الحزمتين كلًّا على حدة. وتفحص الاختبارات الخارجية الـ API بالطريقة التي يستخدمه بها المستدعون، وهي أيضًا الطريقة التي تكتب بها المكتبة القياسية دوال Example القابلة للتشغيل التي تظهر في التوثيق. وكثير من مشاريع Go تجمع بين النوعين، فتستخدم اختبارات داخل الحزمة للتفاصيل الداخلية المعقدة، وحزم _test للسلوك العام.
ما الذي يمكن أن يسبق عبارة package؟
التعليقات والأسطر الفارغة فقط. ونوعان من التعليقات في هذا الموضع لهما معنى عند أدوات Go.
تعليق توثيق الحزمة (package doc comment) يقع مباشرةً فوق package دون سطر فارغ بينهما. والعُرف أن يبدأ بـ Package billing، ويعرضه go doc و pkg.go.dev بوصفه توثيق الحزمة. وكثيرًا ما تضعه الحزم الكبيرة في ملف اسمه doc.go لا يحتوي على شيء آخر. وفي أمر من نوع package main، يصف التعليق البرنامج بدلًا من ذلك، كما في // Api serves the shop's HTTP API.
قيد البناء (build constraint) يحدد هل يُترجم الملف أصلًا أم لا. ويجب أن يأتي قبل عبارة package وأن يتبعه سطر فارغ:
example.gogo//go:build debug package billing import "log" func init() { log.Println("billing: debug build") }
هذا الملف لا يُترجم إلا مع go build -tags debug. والملفات التي تحمل في اسمها لاحقة نظام تشغيل أو معمارية، مثل watch_linux.go أو asm_arm64.go، تُعامل المعاملة نفسها بناءً على اسمها دون أي تعليق.
ويعرض المثال أيضًا init. يمكن للحزمة أن تصرّح عن أي عدد من دوال init، حتى عدة دوال في ملف واحد. تشغّلها Go بعد تهيئة المتغيرات على مستوى الحزمة وقبل أن يبدأ أي شيء يستورد الحزمة. وتكتمل تهيئة الحزمة المستوردة قبل الحزمة التي تستوردها، لذا تعمل main دائمًا في النهاية. احتفظ بـ init للإعدادات الخفيفة مثل تسجيل driver. أما أي شيء قد يفشل فمكانه دالة تُعيد error.
أين يأتي دور LevelUpGo
يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تغطي دورة Packages & Organization الأسماء المُصدَّرة و init وحالة الحزمة ووحدات Go وتنظيم المشاريع، وتبيّن دورة Code Organization كيف تقسّم قاعدة كود تكبر إلى حزم بأسماء واضحة. وإن كنت في البداية، فابدأ بدورة Go Basics. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.
الأسئلة الشائعة
هل يمكن أن يختلف اسم حزمة Go عن اسم مجلدها؟
نعم. لا يقرأ المترجم إلا عبارة package، لذا يُبنى package httpapi داخل مجلد اسمه httpapi-v2 دون مشكلة. والعُرف أن يطابق الاسمُ العنصرَ الأخير من مسار الاستيراد، والاستثناءات الأساسية هي لواحق الإصدار مثل gopkg.in/yaml.v3 (الحزمة yaml) و math/rand/v2 (الحزمة rand).
لماذا يحتاج برنامج Go إلى package main؟
لا يبني الأمر go ملفًا تنفيذيًا إلا من حزمة اسمها main، وتبدأ بيئة التشغيل (runtime) البرنامج باستدعاء الدالة main في تلك الحزمة. وكل حزمة أخرى مكتبة. والحزمة package main التي لا تحتوي على func main() تفشل بالخطأ function main is undeclared in the main package.
هل يمكن أن تحتوي وحدة Go على أكثر من package main؟
نعم، واحدة في كل مجلد. الخدمة التي تضم خادم API وأداة ترحيل (migration) تحتوي عادةً على cmd/api/main.go و cmd/migrate/main.go، وكلاهما يصرّح عن package main. وتبني كلًّا منهما أو تشغّله عبر مجلده: go run ./cmd/api أو go build ./cmd/migrate.
هل يمكن أن يحتوي اسم حزمة Go على شرطات سفلية أو أحرف كبيرة؟
يسمح المترجم بالأمرين، لأن اسم الحزمة يمكن أن يكون أي معرّف صالح ما عدا _. لكن أسلوب Go يوصي بعدم استخدامهما. فمقال Package names في مدونة Go يطلب أسماء قصيرة بأحرف صغيرة «with no under_scores or mixedCaps»، مثل strconv أو httputil.
هل package كلمة مفتاحية في Go؟
نعم. package واحدة من الكلمات المفتاحية المحجوزة الـ 25 في Go، لذا لا يمكنك استخدامها اسمًا لمتغير أو دالة. وعادةً ما يسمّي كود Go المتغير الذي يمثّل حزمة pkg.
المصادر
- The Go Programming Language Specification, Package clause: https://go.dev/ref/spec#Package_clause
- The Go Programming Language Specification, Program initialization and execution: https://go.dev/ref/spec#Program_initialization_and_execution
- The Go Programming Language Specification, Exported identifiers: https://go.dev/ref/spec#Exported_identifiers
- Effective Go, Package names: https://go.dev/doc/effective_go#package-names
- The Go Blog, Package names: https://go.dev/blog/package-names
- Go Doc Comments, Packages and Commands: https://go.dev/doc/comment#package
- Organizing a Go module: https://go.dev/doc/modules/layout
- Command go, Internal Directories: https://pkg.go.dev/cmd/go#hdr-Internal_Directories
- Command go, Build constraints: https://pkg.go.dev/cmd/go#hdr-Build_constraints
- Command go, Test packages: https://pkg.go.dev/cmd/go#hdr-Test_packages
