تجعل الكلمة المفتاحية import الأسماء المُصدَّرة من حزمة أخرى متاحة داخل ملف مصدر Go. تأتي الاستيرادات مباشرةً بعد عبارة package وقبل أي تصريح آخر، ويشير كل استيراد إلى حزمة عبر مسار الاستيراد الخاص بها، مثل "net/http" أو "github.com/jackc/pgx/v5". بعد ذلك تصل إلى محتويات الحزمة عبر اسمها، كما في http.ListenAndServe. ويمكن أن يحمل الاستيراد أيضًا اسمًا مستعارًا (alias) أو المعرّف الفارغ _ أو نقطة .، وكل شكل من هذه الأشكال يغيّر طريقة ربط اسم الحزمة داخل الملف (مواصفات Go).
الخلاصة
- تأتي الاستيرادات بعد
packageوقبل كل شيء آخر. ومعظم الملفات تستخدم كتلة مجمّعة واحدةimport ( ... ). - مسار الاستيراد سلسلة نصية. مسارات المكتبة القياسية قصيرة (
"encoding/json")، وكل ما عداها يبدأ بمسار وحدة (module) منgo.mod. - الاستيراد غير المستخدم خطأ ترجمة:
"os" imported and not used. ويضيف goimports و gopls الاستيرادات ويحذفانها نيابةً عنك. - الصيغة
alias "path"تعيد تسمية الحزمة داخل ملف واحد. استخدمها عندما يتشارك استيرادان الاسم نفسه، مثلhtml/templateوtext/template. - الصيغة
_ "path"تستورد الحزمة من أجل آثارها الجانبية فقط، وهذه الآثار تحدث داخل دوالinitفيها. هكذا تعمل drivers قواعد البيانات وnet/http/pprof. - الصيغة
. "path"تُدخل الأسماء إلى الملف دون بادئة. وأدلة الأسلوب لا تنصح بها، ويعلّم عليها staticcheck بالرمز ST1001، باستثناء حالات نادرة في الاختبارات. - ترفض Go دورات الاستيراد. والحل حزمة مشتركة جديدة، أو واجهة (interface) تُعرَّف في المكان الذي تُستخدم فيه.
- نطاق الاستيراد هو الملف. كل ملف في الحزمة يستورد ما يستخدمه، حتى لو كان ملف مجاور يستورد الحزمة نفسها.
كيف تستورد حزمة في Go؟
اكتب import ثم مسار الحزمة بين علامتي تنصيص مزدوجتين. الملف الذي يحتاج إلى حزمة واحدة يمكنه استخدام سطر واحد، والملف الذي يحتاج إلى عدة حزم يستخدم كتلة مجمّعة بين قوسين:
example.gogopackage main import "fmt" func main() { fmt.Println("ok") }
example.gogopackage main import ( "encoding/json" "log" "net/http" ) type healthResponse struct { Status string `json:"status"` } func main() { http.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(healthResponse{Status: "ok"}) }) log.Fatal(http.ListenAndServe(":8080", nil)) }
الشكلان يعنيان الشيء نفسه، ومعظم كود Go يستخدم الكتلة المجمّعة. وتأتي الاستيرادات دائمًا بعد عبارة package وقبل أي const أو var أو type أو func. وإذا وضعت استيرادًا في موضع أدنى، يتوقف المحلل (parser) بالخطأ syntax error: imports must appear before other declarations. ولا توجد أيضًا طريقة للاستيراد داخل دالة.
وأدوات التنسيق ترتّب الكتلة نيابةً عنك. يرتّب gofmt الأسطر داخل كل مجموعة حسب مسار الاستيراد. أما goimports، الذي تشغّله معظم المحررات عند الحفظ، فيقسم الكتلة أيضًا إلى مجموعتين يفصل بينهما سطر فارغ، المكتبة القياسية أولًا وكل ما عداها ثانيًا:
example.gogoimport ( "context" "fmt" "net/http" "example.com/shop/billing" "github.com/jackc/pgx/v5" )
لا يؤثر الترتيب في البرنامج. لكنه يُبقي الفروقات (diffs) صغيرة، ويُري القارئ بنظرة واحدة أي الاعتماديات تأتي من خارج المكتبة القياسية.
ما مسار الاستيراد في Go؟
مسار الاستيراد هو السلسلة النصية التي تخبر الأمر go بمكان الحزمة. حزم المكتبة القياسية لها مسارات قصيرة لا تحتوي على نقطة في عنصرها الأول، مثل "fmt" أو "net/http" أو "crypto/rand". أما مسار أي حزمة أخرى فيبدأ بمسار وحدة.
حزمك الخاصة تستخدم مسار الوحدة المصرَّح عنه في go.mod مضافًا إليه المجلد. فإذا كان go.mod يحتوي على module example.com/shop، تُستورد الحزمة الموجودة في ./billing بالمسار "example.com/shop/billing". ولا توجد في وضع الوحدات استيرادات نسبية مثل "./billing"، لذا تُستورد الحزمة المحلية دائمًا بمسارها الكامل.
والحزم الخارجية تعمل بالطريقة نفسها، ويحلّها الأمر go عبر go.mod:
- ينزّل
go get github.com/jackc/pgx/v5الوحدة ويضيف سطرrequireإلىgo.mod. - تكتب
"github.com/jackc/pgx/v5"في كتلة الاستيراد. - يضيف
go mod tidyلاحقًا أي وحدة تحتاجها استيراداتك، ويحذف أي وحدة لم يعد شيء يستوردها.
العنصر الأخير من المسار هو اسم الحزمة في العادة، لكن ليس دائمًا. فالمسار "github.com/jackc/pgx/v5" يوفّر حزمة اسمها pgx، والمسار "gopkg.in/yaml.v3" يوفّر yaml. ويشرح مقال الكلمة المفتاحية package في Go العلاقة بين الأسماء والمسارات.
لماذا ترفض Go ترجمة الاستيرادات غير المستخدمة؟
تعامل Go الاستيراد غير المستخدم على أنه خطأ، لا تحذير. اترك "os" في ملف لم يعد يستدعي شيئًا منها، وسيتوقف go build:
example.texttext./main.go:5:2: "os" imported and not used
يشرح Go FAQ السبب. فالاستيراد غير المستخدم يبطئ الترجمة ويضيف اعتمادية لا يحتاجها البرنامج، وفي قاعدة كود كبيرة تتراكم هذه الاستيرادات. والناس يتجاهلون التحذيرات، لذلك جعلتها Go خطأً بدلًا من تحذير. ويقوم go mod tidy بالتنظيف نفسه في go.mod، فيحذف المتطلبات التي لا يستوردها شيء.
عمليًا نادرًا ما تصلح هذه الأخطاء يدويًا. فأداة goimports وأداة gopls، وهي خادم لغة Go الذي يقف خلف دعم Go في VS Code و GoLand، تحذفان الاستيرادات غير المستخدمة وتضيفان الناقصة عند الحفظ. وإذا كنت تصحّح خطأً وتريد إبقاء استيراد ما لدقيقة، فأسند شيئًا منه إلى المعرّف الفارغ، كما في var _ = os.Exit، ثم احذف هذا السطر قبل أن تنفّذ commit.
كيف تعيد تسمية استيراد في Go؟
ضع اسمًا مستعارًا قبل المسار. يحل الاسم المستعار محل اسم الحزمة في ذلك الملف فقط:
example.gogoimport htmltemplate "html/template"
السبب المعتاد هو تعارض الأسماء. فالحزمتان html/template و text/template اسم كل منهما template، وخدمة البريد الإلكتروني تحتاج غالبًا إلى كلتيهما: نص عادي لسطر الموضوع، ومخرجات HTML مُهرَّبة (escaped) لمتن الرسالة. واستيراد الحزمتين دون اسم مستعار يفشل بالخطأ template redeclared in this block. ويكفي اسم مستعار لإحداهما لحل المشكلة:
example.gogopackage main import ( htmltemplate "html/template" "os" "text/template" ) func main() { subject := template.Must(template.New("subject").Parse("Your invoice {{.ID}}\n")) body := htmltemplate.Must(htmltemplate.New("body").Parse("<p>{{.Note}}</p>\n")) data := map[string]string{"ID": "INV-42", "Note": "<script>alert(1)</script>"} subject.Execute(os.Stdout, data) body.Execute(os.Stdout, data) }
توضّح المخرجات سبب وجود الحزمتين منفصلتين. يُطبع الموضوع على شكل Your invoice INV-42، ويُطبع المتن على شكل <p><script>alert(1)</script></p> بعد أن هرّبته html/template.
وتظهر الأسماء المستعارة أيضًا حين تتشارك حزم كثيرة عنصرًا أخيرًا عامًا. فكود Kubernetes يستورد عدة حزم اسم كل منها v1، لذا يكتب corev1 "k8s.io/api/core/v1" و metav1 "k8s.io/apimachinery/pkg/apis/meta/v1". وحين لا يوجد تعارض، احتفظ بالاسم الحقيقي. فالجميع يعرف http.، أما الاسم المستعار المخصص فيُرجع القارئ إلى كتلة الاستيراد ليعرف ما يعنيه.
ماذا يفعل الاستيراد الفارغ _ في Go؟
يحمّل الاستيراد الفارغ حزمة دون أن يربط اسمها. لا يمكنك استدعاء أي شيء منها، لكن الحزمة تُهيَّأ مع ذلك، فتُضبط متغيراتها على مستوى الحزمة وتعمل دوال init فيها. وبعض الحزم تؤدي عملها المفيد داخل init بأن تسجّل نفسها لدى حزمة أخرى.
وأشهر مثال على ذلك driver لـ database/sql:
example.gogopackage main import ( "database/sql" "log" "os" _ "github.com/jackc/pgx/v5/stdlib" ) func main() { db, err := sql.Open("pgx", os.Getenv("DATABASE_URL")) if err != nil { log.Fatal(err) } defer db.Close() }
تستدعي الحزمة stdlib من pgx الدالة sql.Register("pgx", ...) داخل init الخاصة بها. كودك يتعامل مع database/sql فقط، لكن من دون الاستيراد الفارغ لا يُسجَّل الـ driver أبدًا، وتُعيد sql.Open الخطأ sql: unknown driver "pgx" (forgotten import?).
وهذه استيرادات فارغة أخرى ستصادفها في الخدمات الحقيقية:
| الاستيراد | ما تفعله init فيه |
|---|---|
_ "github.com/jackc/pgx/v5/stdlib" | يسجّل الـ driver المسمّى pgx لدى database/sql |
_ "net/http/pprof" | يضيف معالجات التحليل (profiling) تحت /debug/pprof/ إلى http.DefaultServeMux |
_ "image/png" | يسجّل مفكّك ترميز PNG لتتمكن image.Decode من قراءة ملفات PNG |
_ "time/tzdata" | يضمّن قاعدة بيانات المناطق الزمنية للحاويات التي لا تملك واحدة |
_ "embed" | لا شيء وقت التشغيل. لكنه مطلوب قبل أن يعمل //go:embed على متغير من نوع string أو []byte |
حالة embed قاعدة من قواعد أدوات Go وليست أثرًا جانبيًا لدالة init. فإذا حذفت الاستيراد يفشل البناء بالخطأ go:embed requires import "embed" (or import _ "embed", if package is not used).
وترتيب التهيئة يمكن التنبؤ به. فكل حزمة مستوردة تُهيَّأ بالكامل، مع استيراداتها هي، قبل الحزمة التي تستوردها. ومنذ Go 1.21 تحدد المواصفات أيضًا الترتيب بين الحزم غير المترابطة، إذ تُهيَّأ حسب ترتيب مسارات استيرادها. وداخل الحزمة الواحدة، تُضبط المتغيرات على مستوى الحزمة أولًا، ثم تعمل دوال init بترتيب تقديم الملفات إلى المترجم، وهو ترتيب يعتمد فيه الأمر go على أسماء الملفات. وتعمل main في النهاية. والحزمة التي تستوردها عدة حزم أخرى تُهيَّأ مرة واحدة فقط.
ضع الاستيرادات الفارغة في main أو في الحزمة التي تحتاج فعلًا إلى الأثر الجانبي، لا في مكتبة يستوردها الآخرون. فالمكتبة التي تستورد net/http/pprof استيرادًا فارغًا تكشف نقاط نهاية التحليل في كل برنامج يستخدمها، سواء أرادها البرنامج أم لا.
هل ينبغي أن تستخدم استيراد النقطة في Go؟
نادرًا جدًا. يدمج استيراد النقطة الأسماء المُصدَّرة من حزمة داخل الملف، فتستدعيها دون اسم الحزمة:
example.gogoimport . "strings" func normalizeEmail(email string) string { return ToLower(TrimSpace(email)) }
في دالة من ثلاثة أسطر يسهل تتبّع هذا. أما في ملف طويل، فالقارئ الذي يرى ToLower لا يستطيع أن يعرف هل هي معرّفة في هذه الحزمة، أم من أي استيراد جاءت. وصفحة Go Code Review Comments توصي بعدم استخدامه، ويعلّم عليه staticcheck:
example.texttextmain.go:3:8: should not use dot imports (ST1001)
الاستخدام المقبول الوحيد هو اختبار يجب أن يقع خارج الحزمة التي يختبرها بسبب دورة استيراد. وتضرب Code Review Comments مثالًا بالحزمة package foo_test التي تستورد bar/testutil، وهذه بدورها تستورد foo. واستيراد foo بالنقطة يجعل ذلك الاختبار يُقرأ كأنه داخل الحزمة. وبعض أُطر الاختبار، مثل Ginkgo و Gomega، توثّق أيضًا استيراد النقطة من أجل الـ matchers الخاصة بها. وفي غير هذه الحالات، اكتب اسم الحزمة.
ما دورة الاستيراد في Go؟
دورة الاستيراد (import cycle) هي حزمتان أو أكثر تستورد كل منهما الأخرى، مباشرةً أو عبر سلسلة. وتمنعها Go، لذا إذا استوردت billing الحزمة customers بينما تستورد customers الحزمة billing، يفشل البناء:
example.texttextpackage example.com/shop/billing imports example.com/shop/customers from billing.go imports example.com/shop/billing from customers.go: import cycle not allowed
تُبقي هذه القاعدة ترتيب التهيئة محددًا بوضوح والبناء سريعًا، لأن المترجم يستطيع دائمًا ترجمة الحزمة بعد كل ما تعتمد عليه. وهي تخبرك أيضًا بشيء عن التصميم، فالدورة تعني عادةً أن الحزمتين في الحقيقة حزمة واحدة، أو أن كلتيهما تعتمد على شيء مكانه حزمة ثالثة.
هناك حلّان شائعان:
- انقل الجزء المشترك إلى حزمة خاصة به. إذا كانت
billingوcustomersتحتاجان كلتاهما إلى نوعCustomerID، فضعه في حزمة صغيرة مثلshop/idsأوshop/domainلا تستورد أيًّا منهما. - عرّف واجهة في المكان الذي تُستخدم فيه. إذا كانت
billingتحتاج فقط إلى معرفة البريد الإلكتروني لعميل ما، فيمكنها أن تصرّح بما تحتاجه وتتركmainتمرّر التنفيذ الحقيقي:
example.gogopackage billing import "context" // CustomerLookup is the one thing billing needs from the customers package. type CustomerLookup interface { Email(ctx context.Context, customerID string) (string, error) } type Service struct { customers CustomerLookup } func NewService(customers CustomerLookup) *Service { return &Service{customers: customers} }
الآن لم تعد billing تستورد customers، وتستطيع customers أن تستورد billing بحرية. وأي نوع يملك الدالة Email المطابقة يحقق الواجهة دون أن يذكرها، فتبقى الاعتمادية في اتجاه واحد فقط.
ويطبّق الأمر go قاعدة قريبة على package main. فلا يمكن لأي حزمة استيرادها، ومحاولة ذلك تعطي الخطأ import "example.com/shop/cmd/api" is a program, not an importable package.
هل نطاق الاستيراد في Go هو الملف أم الحزمة؟
نطاق الاستيراد هو الملف. فالاستيراد يربط اسم الحزمة داخل الملف الذي يحتويه فقط، مع أن كل اسم آخر على مستوى الحزمة مشترك بين جميع ملفاتها. هنا يستورد server.go الحزمة log:
example.gogopackage main import "log" func main() { log.Println("server starting") runJobs() }
ويستدعي jobs.go في الحزمة نفسها log دون أن يستوردها:
example.gogopackage main func runJobs() { log.Println("running jobs") }
الدالة runJobs مرئية في server.go لأن الدوال على مستوى الحزمة مشتركة. أما استيراد log فليس مشتركًا، لذا يفشل البناء:
example.texttext./jobs.go:4:2: undefined: log
كل ملف يسرد استيراداته الخاصة، ويضيفها goimports نيابةً عنك. ويعني ذلك أيضًا أن اسم الاستيراد قد يتعارض مع تصريح على مستوى الحزمة في ملف آخر. فإذا صرّحت عن var log = ... في ملف بينما يستورد ملف آخر log، ستحصل على الخطأ log already declared through import of package log ("log").
والأسماء المستعارة نطاقها الملف كذلك. فإعادة التسمية mrand "math/rand/v2" في ملف واحد لا تؤثر في بقية ملفات الحزمة.
وتقيّد Go أيضًا الحزم التي يمكنك استيرادها من الأساس. فالحزمة الموجودة تحت مجلد internal/ لا يستوردها إلا الكود الموجود تحت المجلد الأب لـ internal. ويغطي مقال الكلمة المفتاحية package في Go المجلد internal/ وكيف يعمل الظهور (visibility) بين الحزم.
أين يأتي دور LevelUpGo
يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تغطي دورة Packages & Organization الأسماء المُصدَّرة و init وحالة الحزمة ووحدات Go والاعتماديات، وأوامر الوحدات التي تشغّلها كل يوم مثل go get و go mod tidy. وإن كنت في البداية، فابدأ بدورة Go Basics. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.
الأسئلة الشائعة
هل import كلمة مفتاحية في Go؟
نعم. import واحدة من الكلمات المفتاحية المحجوزة الـ 25 في Go، لذا لا يمكنك استخدامها اسمًا لمتغير أو دالة أو حزمة. ولا يمكن أن تظهر إلا في تصريحات الاستيراد أعلى الملف.
هل يمكنك استيراد حزمة داخل دالة في Go؟
لا. الاستيرادات مسموحة فقط في المستوى الأعلى من الملف، مباشرةً بعد عبارة package. والكلمة import داخل جسم دالة تفشل بالخطأ syntax error: unexpected keyword import. وإذا كنت تحتاج إلى حزمة في دالة واحدة فقط، فاستوردها أعلى الملف على أي حال.
كيف تستورد حزمة محلية في Go؟
استخدم مسار الوحدة من go.mod متبوعًا بمجلد الحزمة. ففي وحدة اسمها example.com/shop، تُستورد الحزمة الموجودة في ./internal/pricing بالمسار "example.com/shop/internal/pricing". ولا تدعم وحدات Go الاستيرادات النسبية مثل "./pricing".
ما الفرق بين import _ و import . في Go؟
الصيغة import _ "path" تشغّل تهيئة الحزمة لكنها لا تعطيك أي طريقة للإشارة إليها. وهي مخصصة للآثار الجانبية مثل تسجيل driver لقاعدة بيانات. أما الصيغة import . "path" فتفعل العكس، إذ تضع كل الأسماء المُصدَّرة من الحزمة مباشرةً في ملفك، فتستدعيها دون بادئة. الاستيراد الفارغ شائع في كود Go الاصطلاحي، بينما لا يُنصح باستيراد النقطة.
هل يهم ترتيب الاستيرادات في Go؟
لا. لا يهتم المترجم بترتيب الأسطر في كتلة الاستيراد، وترتيب التهيئة يحدده مخطط الاعتماديات، لا الطريقة التي تسرد بها الاستيرادات. يرتّبها gofmt حسب المسار، ويفصل goimports المكتبة القياسية عن الحزم الأخرى، لكن ذلك لتسهيل القراءة فقط.
المصادر
- The Go Programming Language Specification, Import declarations: https://go.dev/ref/spec#Import_declarations
- The Go Programming Language Specification, Package initialization: https://go.dev/ref/spec#Package_initialization
- Effective Go, The blank identifier in imports: https://go.dev/doc/effective_go#blank_import
- Go FAQ, Unused variables and imports: https://go.dev/doc/faq#unused_variables_and_imports
- Command go, Import path syntax: https://pkg.go.dev/cmd/go#hdr-Import_path_syntax
- Command go, Internal directories: https://pkg.go.dev/cmd/go#hdr-Internal_Directories
- Package database/sql: https://pkg.go.dev/database/sql
- Package embed: https://pkg.go.dev/embed
- Go Code Review Comments, Import blank and import dot: https://go.dev/wiki/CodeReviewComments#import-blank
- goimports: https://pkg.go.dev/golang.org/x/tools/cmd/goimports
- Staticcheck ST1001: https://staticcheck.dev/docs/checks/#ST1001
