مثل العديد من لغات البرمجة الحديثة، تُنظَّم شيفرة Go أيضًا في صورة حزم شيفرة (code packages). ولاستخدام عناصر الشيفرة المصدَّرة (الدوال والأنواع والمتغيرات والثوابت المسمّاة، وغيرها) في حزمة محددة، يجب أولًا استيراد تلك الحزمة، باستثناء حزمة الشيفرة القياسية builtin (وهي حزمة كونية (universe package)). ستوضّح هذه المقالة حزم الشيفرة واستيرادات الحزم في Go.
مقدمة عن استيراد الحزم
لنطّلع على برنامج صغير يستورد حزمة شيفرة قياسية. (افترض أن الشيفرة المصدرية لهذا البرنامج محفوظة في ملف باسم simple-import-demo.go.)
package main
import "fmt"
func main() {
fmt.Println("Go has", 25, "keywords.")
}
بعض التوضيحات:
- السطر الأول يحدد اسم الحزمة التي تحتوي الملف المصدري
simple-import-demo.go. ويجب أن توضع دالة الدخولmainلأي برنامج في حزمة اسمهاmain. - السطر الثالث يستورد الحزمة القياسية
fmtباستخدام الكلمة المفتاحيةimport. والمعرّفfmtهو اسم الحزمة. وهو يُستخدم أيضًا كاسم استيراد لهذه الحزمة القياسية ويمثلها في نطاق الملف المصدري الحاوي. (ستُشرح أسماء الاستيراد في قسم لاحق.) وهناك العديد من دوال التنسيق المعرَّفة في هذه الحزمة القياسية لتستخدمها الحزم الأخرى. ودالةPrintlnإحداها. وهي تطبع التمثيلات النصية لعدد عشوائي من المعطيات إلى المخرج القياسي. - السطر السادس يستدعي الدالة
Println. لاحظ أن اسم الدالة مسبوق بـfmt.في الاستدعاء، حيثfmtهو اسم الحزمة التي تحتوي الدالة المستدعاة. وتسمى الصيغةaImportName.AnExportedIdentifierمعرّفًا مؤهَّلًا. ويسمىAnExportedIdentifierمعرّفًا غير مؤهَّل. - استدعاء الدالة
fmt.Printlnليس له متطلبات بشأن معطياته، لذا ستُستنتج معطياته الثلاثة في هذا البرنامج كقيم من أنواعها الافتراضية،stringوintوstring. - في كل استدعاء لـ
fmt.Println، يُدرَج محرف مسافة بين كل تمثيلين نصيين متتاليين، ويُطبع محرف سطر جديد في النهاية.
عند تشغيل هذا البرنامج ستحصل على المخرج التالي:
$ go run simple-import-demo.go
Go has 25 keywords.
يرجى الملاحظة أن عناصر الشيفرة المصدَّرة فقط في حزمة ما يمكن استخدامها في الملف المصدري الذي يستورد تلك الحزمة. ويستخدم عنصر الشيفرة المصدَّر معرّفًا مصدَّرًا كاسم له. على سبيل المثال، المحرف الأول من المعرّف Println حرف كبير (لذا فإن الدالة Println مصدَّرة)، ولهذا يمكن استخدام الدالة Println المعرَّفة في الحزمة القياسية fmt في البرنامج المثال أعلاه.
الدالتان المدمجتان print وprintln لهما وظائف مشابهة للدوال المقابلة لهما في الحزمة القياسية fmt. ويمكن استخدام الدوال المدمجة دون استيراد أي حزم.
لاحظ أن الدالتين المدمجتين print وprintln لا يُوصى باستخدامهما في بيئة الإنتاج، لأنه لا يوجد ضمان لبقائهما في إصدارات Go المستقبلية.
جميع الحزم القياسية مُدرجة هنا (القائمة الرسمية) أو هنا (قائمة غير رسمية، يصونها Go 101). ويمكننا أيضًا تشغيل خادم محلي لعرض توثيق Go.
يسمى استيراد الحزمة أيضًا تعريف استيراد (import declaration) رسميًا في Go. وتعريف الاستيراد مرئي فقط للملف المصدري الذي يحتوي تعريف الاستيراد. وهو غير مرئي لملفات المصدر الأخرى في الحزمة نفسها.
لنطّلع على مثال آخر:
package main
import "fmt"
import "math/rand"
func main() {
fmt.Printf("Next pseudo-random number is %v.\n", rand.Uint32())
}
يستورد هذا المثال حزمة قياسية أخرى، هي الحزمة math/rand، وهي حزمة فرعية من الحزمة القياسية math. وتوفر هذه الحزمة بعض الدوال لتوليد أعداد شبه عشوائية.
بعض التوضيحات:
- في هذا المثال، يُستخدم اسم الحزمة
randكاسم استيراد للحزمة القياسية المستوردةmath/rand. وسيُرجع استدعاءrand.Uint32()عددًا صحيحًا عشوائيًا من النوعuint32. Printfدالة أخرى شائعة الاستخدام في الحزمة القياسيةfmt. ويجب أن يأخذ استدعاء الدالةPrintfمعطىً واحدًا على الأقل. ويجب أن يكون المعطى الأول لاستدعاء الدالةPrintfقيمة من النوعstringتحدد تنسيق النتيجة المطبوعة. ويسمى%vفي المعطى الأول فعل تنسيق (format verb)، وسيُستبدل بالتمثيل النصي للمعطى الثاني. وكما تعلمنا في مقال الأنواع الأساسية وقيمها الحرفية، فإن\nفي قيمة نصية حرفية محاطة بعلامات اقتباس مزدوجة سيُهرَّب كمحرف سطر جديد.
سيُخرج البرنامج أعلاه دائمًا:
Next pseudo-random number is 2596996162.
ملاحظة: قبل Go 1.20، إذا أردنا أن يُنتج البرنامج أعلاه عددًا عشوائيًا مختلفًا في كل تشغيل، وجب علينا تعيين بذرة (seed) مختلفة باستدعاء الدالة rand.Seed عند بدء البرنامج مباشرة.
إذا استُوردت حزم متعددة إلى ملف مصدري واحد، يمكننا تجميعها في تعريف استيراد واحد بوضعها بين قوسين ().
مثال:
package main
// يمكن استيراد حزم متعددة معًا.
import (
"fmt"
"math/rand"
)
func main() {
fmt.Printf("Next pseudo-random number is %v.\n", rand.Uint32())
}
بعض التوضيحات:
- يستورد هذا المثال حزمة أخرى، هي الحزمة القياسية
time، التي توفر العديد من الأدوات المتعلقة بالوقت. - الدالة
time.Now()تُرجع الوقت الحالي، كقيمة من النوعtime.Time.
المزيد عن أفعال التنسيق في fmt.Printf
كما ذُكر أعلاه، إذا وُجد فعل تنسيق واحد في المعطى الأول لاستدعاء fmt.Printf، فسيُستبدل بالتمثيل النصي للمعطى الثاني. في الواقع، يمكن أن توجد أفعال تنسيق متعددة في المعطى الأول من النوع string. وسيُستبدل فعل التنسيق الثاني بالتمثيل النصي للمعطى الثالث، وهكذا.
في Go 101، ستُستخدم فقط أفعال التنسيق المدرجة تاليًا.
%v، وسيُستبدل بالتمثيل النصي العام للمعطى المقابل.%T، وسيُستبدل باسم النوع أو القيمة الحرفية للنوع الخاصة بالمعطى المقابل.%x، وسيُستبدل بالتمثيل النصي الست عشري للمعطى المقابل. لاحظ أن التمثيلات النصية الست عشرية لقيم بعض أنواع الأنواع غير معرَّفة. وبشكل عام، يجب أن تكون المعطيات المقابلة لـ%xقيماً نصية أو أعدادًا صحيحة أو مصفوفات أعداد صحيحة أو شرائح أعداد صحيحة (ستُشرح المصفوفات والشرائح في مقال لاحق).%s، وسيُستبدل بالتمثيل النصي للمعطى المقابل. ويجب أن يكون المعطى المقابل قيمة نصية أو شريحة بايتات.- فعل التنسيق
//يمثل علامة النسبة المئوية.
مثال:
package main
import "fmt"
func main() {
a, b := 123, "Go"
fmt.Printf("a == %v == 0x%x, b == %s\n", a, a, b)
fmt.Printf("type of a: %T, type of b: %T\n", a, b)
fmt.Printf("1// 50// 99//\n")
}
المخرج:
a == 123 == 0x7b, b == Go
type of a: int, type of b: string
1% 50% 99%
لمزيد من أفعال التنسيق في Printf، يرجى قراءة توثيق حزمة fmt على الإنترنت، أو عرض التوثيق نفسه بتشغيل خادم توثيق محلي. ويمكننا أيضًا تشغيل go doc fmt لعرض توثيق الحزمة القياسية fmt، وتشغيل go doc fmt.Printf لعرض توثيق الدالة fmt.Printf، في الطرفية.
مجلد الحزمة ومسار استيراد الحزمة وتبعيات الحزمة
قد تتكون حزمة الشيفرة من عدة ملفات مصدرية. وتقع هذه الملفات المصدرية في المجلد نفسه. ويجب أن تنتمي الملفات المصدرية في مجلد واحد (دون المجلدات الفرعية) إلى الحزمة نفسها. إذن، يقابل المجلد حزمة شيفرة واحدة، والعكس صحيح. ويسمى المجلد الذي يحتوي الملفات المصدرية لحزمة شيفرة بمجلد الحزمة.
بالنسبة إلى سلسلة أدوات Go (Go Toolchain)، تُعتبر الحزمة التي يحتوي مسار استيرادها على اسم مجلد internal حزمة خاصة. ولا يمكن استيرادها إلا من الحزم الموجودة في المجلد الأب المباشر لمجلد internal وما تحته. على سبيل المثال، الحزمتان .../a/b/c/internal/d/e/f و.../a/b/c/internal لا يمكن استيرادهما إلا من الحزم التي تحتوي مسارات استيرادها على البادئة .../a/b/c.
عندما يستورد ملف مصدري في حزمة ما حزمة أخرى، نقول إن الحزمة المستورِدة تعتمد على الحزمة المستورَدة.
لا تدعم Go تبعيات الحزم الدائرية. فإذا كانت الحزمة a تعتمد على الحزمة b وكانت الحزمة b تعتمد على الحزمة c، فلا يمكن للملفات المصدرية في الحزمة c استيراد الحزمتين a وb، ولا يمكن للملفات المصدرية في الحزمة b استيراد الحزمة a.
وبالتأكيد، لا يمكن للملفات المصدرية في حزمة ما استيراد الحزمة نفسها، ولا حاجة لها إلى ذلك.
لاحقًا، سنسمي الحزم المسماة بـ main والتي تحتوي دوال الدخول main حزم برامج (أو حزم أوامر)، ونسمي الحزم الأخرى حزم مكتبات. وحزم البرامج غير قابلة للاستيراد. ويجب أن يحتوي كل برنامج Go على حزمة برنامج واحدة فقط لا غير.
لا يُشترط أن يكون اسم مجلد الحزمة مطابقًا لاسم الحزمة. لكن بالنسبة إلى حزمة مكتبة، فإن اختلاف اسم الحزمة عن اسم مجلد الحزمة سيُربك مستخدمي الحزمة. وسبب الارتباك هو أن مسار الاستيراد الافتراضي للحزمة هو اسم الحزمة، لكن ما يحتويه مسار استيراد الحزمة هو اسم مجلد الحزمة. لذا يرجى محاولة جعل الاسمين متطابقين لكل حزمة مكتبة.
ومن ناحية أخرى، يُوصى بإعطاء كل مجلد حزمة برنامج اسمًا ذا معنى بدلًا من اسم حزمته main.
دوال init
يمكن تعريف عدة دوال باسم init في حزمة واحدة، وحتى في ملف شيفرة مصدرية واحد. ويجب ألا يكون للدوال المسماة init أي وسائط دخل ولا نتائج إرجاع.
لاحظ أنه في الكتلة العليا على مستوى الحزمة، لا يمكن استخدام المعرّف init إلا في تعريفات الدوال. فلا يمكننا تعريف متغيرات/ثوابت/أنواع على مستوى الحزمة أسماؤها init.
في وقت التشغيل، ستُستدعى كل دالة init (تسلسليًا) مرة واحدة فقط لا غير (قبل استدعاء دالة الدخول main). لذا فإن معنى دوال init يشبه كثيرًا كتل التهيئة الساكنة (static initializer blocks) في Java.
إليك مثالًا بسيطًا يحتوي على دالتي init:
package main
import "fmt"
func init() {
fmt.Println("hi,", bob)
}
func main() {
fmt.Println("bye")
}
func init() {
fmt.Println("hello,", smith)
}
func titledName(who string) string {
return "Mr. " + who
}
var bob, smith = titledName("Bob"), titledName("Smith")
مخرج هذا البرنامج:
hi, Mr. Bob
hello, Mr. Smith
bye
ترتيب تهيئة الموارد
في وقت التشغيل، ستُحمَّل كل حزمة بعد جميع الحزم التي تعتمد عليها. وستُحمَّل كل حزمة مرة واحدة فقط لا غير.
ستُستدعى جميع دوال init في كل الحزم المشاركة في البرنامج تسلسليًا. ودالة init في حزمة مستورِدة ستُستدعى بالتأكيد بعد جميع دوال init المعرَّفة في الحزم التي تعتمد عليها الحزمة المستورِدة. وستُستدعى جميع دوال init قبل استدعاء دالة الدخول main.
ترتيب استدعاء دوال init في الملف المصدري نفسه من الأعلى إلى الأسفل. وتوصي مواصفات Go، دون أن تشترط، باستدعاء دوال init في ملفات مصدرية مختلفة من الحزمة نفسها وفق الترتيب الأبجدي لأسماء ملفاتها المصدرية الحاوية. لذا ليس من الجيد وجود علاقات تبعية بين دالتي init في ملفين مصدريين مختلفين.
تُهيَّأ جميع المتغيرات المعرَّفة على مستوى الحزمة في حزمة ما قبل استدعاء أي دالة init معرَّفة في الحزمة نفسها.
سيحاول وقت تشغيل Go تهيئة المتغيرات على مستوى الحزمة في حزمة ما وفق ترتيب تعريفها، لكن المتغير على مستوى الحزمة سيُهيَّأ بالتأكيد بعد جميع المتغيرات التي يعتمد عليها. على سبيل المثال، في مقتطف الشيفرة التالي، تحدث تهيئات المتغيرات الأربعة على مستوى الحزمة بالترتيب y وz وx وw.
func f() int {
return z + y
}
func g() int {
return y/2
}
var (
w = x
x, y, z = f(), 123, g()
)
لمعرفة القاعدة الأكثر تفصيلًا لترتيب تهيئة المتغيرات على مستوى الحزمة، يرجى قراءة مقال ترتيب تقييم التعابير.
الصيغ الكاملة لاستيراد الحزم
في الواقع، الصيغة الكاملة لتعريف الاستيراد هي
import importname "path/to/package"
حيث importname اختياري، وقيمته الافتراضية هي اسم الحزمة المستوردة (وليس اسم مجلدها).
في الواقع، في تعريفات الاستيراد المستخدمة أعلاه، حُذفت أجزاء importname كلها لأنها مطابقة لأسماء الحزم المعنية. وتعريفات الاستيراد هذه مكافئة للتعريفات التالية:
import fmt "fmt" // يكافئ import "fmt"
import rand "math/rand" // يكافئ import "math/rand"
import time "time" // يكافئ import "time"
إذا وُجد جزء importname في تعريف استيراد، فيجب أن تكون الرموز البادئة المستخدمة في المعرّفات المؤهَّلة هي importname بدلًا من اسم الحزمة المستوردة.
لا تُستخدم صيغة تعريف الاستيراد الكاملة على نطاق واسع. لكن يجب علينا أحيانًا استخدامها. على سبيل المثال، إذا استورد ملف مصدري حزمتين لهما الاسم نفسه، لتجنب إرباك المصرِّف، يجب علينا استخدام صيغة الاستيراد الكاملة لتعيين importname مخصص لإحدى الحزمتين على الأقل.
إليك مثالًا على استخدام صيغ تعريف الاستيراد الكاملة.
package main
import (
format "fmt"
random "math/rand"
)
func main() {
format.Print("A random number: ", random.Uint32(), "\n")
// السطر التالي يفشل في التصريف،
// لأن "rand" غير معرَّف.
/*
fmt.Print("A random number: ", rand.Uint32(), "\n")
*/
}
بعض التوضيحات:
- يجب علينا استخدام
formatوrandomكرمزين بادئين في المعرّفات المؤهَّلة، بدلًا من اسمي الحزمتين الحقيقيينfmtوrand. Printدالة أخرى في الحزمة القياسيةfmt. ومثل استدعاءات الدالةPrintln، يمكن لاستدعاء الدالةPrintأن يأخذ عددًا عشوائيًا من المعطيات. وستطبع التمثيلات النصية للمعطيات الممرَّرة، واحدًا واحدًا. وإذا لم يكن معطيان متتاليان قيمتين نصيتين، فسيُدرَج محرف مسافة تلقائيًا بينهما في نتيجة الطباعة.
يمكن أن يكون importname في تعريف الاستيراد الكامل نقطة (.). وتسمى هذه الاستيرادات استيرادات نقطية (dot imports). ولاستخدام العناصر المصدَّرة في الحزم المستوردة استيرادًا نقطيًا، يجب حذف الجزء البادئ في المعرّفات المؤهَّلة.
مثال:
package main
import (
. "fmt"
. "time"
)
func main() {
Println("Current time:", Now())
}
في المثال أعلاه، يجب استخدام Println بدلًا من fmt.Println، وNow بدلًا من time.Now.
بشكل عام، تقلل الاستيرادات النقطية من مقروئية الشيفرة، لذا لا يُوصى باستخدامها في المشاريع الرسمية.
يمكن أن يكون importname في تعريف الاستيراد الكامل المعرّف الفارغ (blank identifier) وهو _. وتسمى هذه الاستيرادات استيرادات مجهولة (anonymous imports) (وتسميها بعض المقالات في مواضع أخرى استيرادات فارغة (blank imports)). ولا يمكن للملفات المصدرية المستورِدة استخدام عناصر الشيفرة المصدَّرة في الحزم المستوردة استيرادًا مجهولًا. والغرض من الاستيرادات المجهولة هو تهيئة الحزم المستوردة (إذ ستُستدعى كل دالة من دوال init في الحزم المستوردة استيرادًا مجهولًا مرة واحدة).
في المثال التالي، ستُستدعى جميع دوال init المعرَّفة في الحزمة القياسية net/http/pprof قبل استدعاء دالة الدخول main.
package main
import _ "net/http/pprof"
func main() {
... // افعل بعض الأشياء
}
يجب استخدام كل استيراد غير مجهول مرة واحدة على الأقل
باستثناء الاستيرادات المجهولة، يجب استخدام الاستيرادات الأخرى مرة واحدة على الأقل. على سبيل المثال، المثال التالي يفشل في التصريف.
package main
import (
"net/http" // خطأ: مستوردة ولم تُستخدم
. "time" // خطأ: مستوردة ولم تُستخدم
)
import (
format "fmt" // لا بأس: تُستخدم مرة واحدة أدناه
_ "math/rand" // لا بأس: ليس مطلوبًا استخدامه
)
func main() {
format.Println() // استخدام الحزمة المستوردة "fmt"
}
الوحدات
الوحدة (module) هي مجموعة من عدة حزم. وبعد تنزيلها محليًا، تكون هذه الحزم جميعها محتواة في المجلد نفسه، ويسمى المجلد الجذر للوحدة. وقد يكون للوحدة إصدارات عديدة تتبع مواصفة الإصدار الدلالي (Semantic Versioning). ولمعرفة المزيد من المفاهيم المتعلقة بالوحدات وكيفية إدارة الوحدات واستخدامها، يرجى قراءة التوثيق الرسمي.