العودة إلى المدونة

الكلمة المفتاحية package في Go: شرح package main وقواعد التسمية مع أمثلة

يبدأ كل ملف Go بعبارة package. تعرّف على وظيفة package main، وعلى علاقة أسماء الحزم بالمجلدات ومسارات الاستيراد، وعلى قواعد التسمية في Go.

الكلمة المفتاحية package في Go: شرح package main وقواعد التسمية مع أمثلة

تحدد الكلمة المفتاحية 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.gogo
package 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.texttext
runtime.main_main·f: function main is undeclared in the main package

وتشغيل حزمة مكتبة بالأمر go run ./billing يفشل قبل أن تبدأ الترجمة أصلًا:

example.texttext
package 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.texttext
found 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/httphttpمتطابقان
gopkg.in/yaml.v3yamlيحمل المسار لاحقة الإصدار .v3
math/rand/v2randمجلد الإصدار الرئيسي /v2
github.com/jackc/pgx/v5pgxمجلد الإصدار الرئيسي /v5

عندما يتشارك استيرادان الاسم نفسه، تعيد تسمية أحدهما في كتلة الاستيراد. فالكود الذي ينشئ رموز الجلسات (session tokens) ويضيف أيضًا تذبذبًا عشوائيًا (jitter) لإعادة المحاولة يحتاج غالبًا إلى crypto/rand و math/rand/v2 معًا، وكلاهما اسمه rand:

example.gogo
package 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.texttext
use of internal package example.com/shop/internal/pricing not allowed

يشرح مقال هيكلة مشاريع Go متى تلجأ إلى internal/ وكيف تنظّم cmd/ وحزم المكتبات في خدمة حقيقية.

ما حزمة _test في Go؟

يمكن لملفات الاختبار، أي الملفات التي تنتهي بـ _test.go، أن تستخدم أحد اسمين للحزمة. يضع package billing الاختبار داخل الحزمة، فيستطيع استدعاء الدوال غير المُصدَّرة:

example.gogo
package 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.gogo
package 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.

المصادر

اكتب Go كما يكتبها مهندس أول

دروس تفاعلية في متصفحك. الدروس الأولى مجانية.

جرّب درسًا مجانيًاأو أنشئ حسابًا مجانيًا