تُستخدَم الدالة المُعرّفة مسبقًا ()init
في لغة جو لجعل شيفرة مُحدَّدة تُنفّذ قبل أيّ شيفرة أخرى ضمن الحزمة الخاصة بك، إذ ستُنفَّذ هذه الشيفرة عند استيراد الحزمة مباشرةً، وبالتالي يمكنك استخدام هذه الدالة عندما تحتاج إلى تهيئة تطبيقك في حالة معينة مثل أن يكون لديك إعدادات أوليّة محددة أو مجموعة من الموارد التي يحتاجها تطبيقك لكي يبدأ.
تُستخدم أيضًا عند استيراد تأثير جانبي side effect، وهي تقنية تستخدَم لضبط حالة البرنامج من خلال استيراد حزمة معينة، ويُستخدَم هذا غالبًا لتسجيل register
حزمة مع أخرى للتأكد من عمل البرنامج بطريقة صحيحة.
يُعَدّ استخدام الدالة ()init
مفيدًا، ولكنه يجعل من قراءة الشيفرة أمرًا صعبًا في بعض الأحيان وذلك لأنّ نسخة ()init
التي يصعُب العثور عليها ستؤثر إلى حد كبير في ترتيب تنفيذ شفرات البرنامج، وبالتالي لا بدّ من فهم هذه الدالة جيدًا لاستخدامها بطريقة صحيحة.
سنتعلم في هذا المقال استخدام الدالة ()init
لإعداد وتهيئة متغيرات حزمة معيّنة وعمليات حسابية تجرى لمرة واحدة وتسجيل حزمة لاستخدامها مع حزمة أخرى.
المتطلبات
ستحتاج من أجل بعض الأمثلة في هذا المقال إلى مساحة عمل خاصة مُهيئة وجاهزة، فإذا لم يكن لديك واحدة، فيمكنك اتباع المقالات السابقة حيث تحدثنا عن كيفية إعداد وتثبيت بيئة برمجة محلية في لغة جو. إذ يستخدِم هذا المقال بنية الملفات التالية:
. ├── bin │ └── src └── github.com └── gopherguides
التصريح عن الدالة ()init
بمجرّد التصريح عن أيّ دالة ()init
سيُنفّذها مصرِّف جو قبل أيّ شيفرة أخرى في الحزمة، وسنوضِّح في هذا القسم كيفية تعريف هذه الدالة وكيف تؤثر على تشغيل الحزمة، لذا دعونا الآن نلقي نظرةً على مثال لا يتضمن هذه الدالة:
package main import "fmt" var weekday string func main() { fmt.Printf("Today is %s", weekday) }
صرّحنا في هذا البرنامج عن متغير عام weekday
من النوع string
، وبما أننا لم نُهيّئ هذا المتغير بأيّ قيمة، فستكون القيمة الافتراضية لها هي السلسلة الفارغة، وبالتالي لن نرى أيّ شيء عند طباعتها، ولنشغّل هذه الشيفرة كما يلي:
$ go run main.go
سيكون الخرج كما يلي:
Today is
يمكننا إعطاء قيمة أوليّة إلى المتغير weekday
من خلال الدالة ()init
بحيث نُهيّئها بقيمة اليوم الحالي، لذا سنعدِّل على الشيفرة السابقة كما يلي:
package main import ( "fmt" "time" ) var weekday string func init() { weekday = time.Now().Weekday().String() } func main() { fmt.Printf("Today is %s", weekday) }
استخدمنا في هذه الشيفرة الحزمة time
للحصول على اليوم الحالي من خلال كتابة ()Now().Weekday().String
ثم استخدمنا الدالة ()init
لتهيئة المتغير weekday
بها، والآن إذا شغّلنا البرنامج، فسنحصل على خرج مُختلف عن المرة السابقة:
Today is Monday
كان الهدف من هذا المثال هو توضيح عمل الدالة، إلا أن استخدامها الأكثر شيوعًا يكون عند استيراد حزمة، فقد نكون بحاجة إلى إتمام بعض عمليات التهيئة أو بعض العمليات الأولية قبل استخدام الحزمة، لذا دعنا ننشئ برنامجًا سيتطلب تهيئة محددة للحزمة لتعمل على النحو المنشود.
تهيئة الحزم عند استيرادها
بدايةً، سنكتب برنامجًا بسيطًا يختار عشوائيًا عنصرًا من شريحة ما ويطبعها، ولن نستخدِم هنا الدالة ()init
لكي نُبيّن المشكلة التي ستحدث وكيف ستحلّها هذه الدالة فيما بعد، لذا أنشئ مجلدًا اسمه creature من داخل المجلد /src/github.com/gopherguides كما يلي:
$ mkdir creature
أنشئ بداخله الملف creature.go:
$ nano creature/creature.go
ضع بداخل الملف التعليمات التالية:
package creature import ( "math/rand" ) var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"} func Random() string { i := rand.Intn(len(creatures)) return creatures[i] }
يُعرِّف هذا الملف متغيرًا يسمى creatures
يحتوي على مجموعة من أسماء الكائنات البحرية على أساس قيم لهذه الشريحة، كما يحتوي هذا الملف على دالة عشوائية مُصدّرة تُعيد قيمةً عشوائيةً من هذه الشريحة. احفظ الملف واخرج منه.
سننشئ الآن الحزمة cmd
التي سنستخدِمها لكتابة الدالة ()main
واستدعاء الحزمة creature
، ولأجل ذلك يجب أن نُنشئ مجلدًا اسمه cmd بجانب المجلد creature كما يلي:
$ mkdir cmd
سنُنشئ بداخله الملف main.go:
$ nano cmd/main.go
أضف المحتويات التالية إلى المجلد:
package main import ( "fmt" "github.com/gopherguides/creature" ) func main() { fmt.Println(creature.Random()) fmt.Println(creature.Random()) fmt.Println(creature.Random()) fmt.Println(creature.Random()) }
استوردنا هنا الحزمة creature
، ثم استدعينا الدالة ()creature.Random
بداخل الدالة ()main
للحصول على عنصر عشوائي من الشريحة وطباعته أربع مرات. احفظ الملف main.go ثم أغلقه.
انتهينا الآن من كتابة الشيفرة، لكن قبل أن نتمكن من تشغيل هذا البرنامج، سنحتاج أيضًا إلى إنشاء ملفَين من ملفات الضبط configuration حتى تعمل التعليمات البرمجية بطريقة صحيحة.
تُستخدَم وحدات لغة جو Go Modules لضبط اعتماديات الحزمة لاستيراد الموارد، إذ تُعَدّ وحدات لغة جو ملفات ضبط موضوعة في مجلد الحزمة الخاص بك والتي تخبر المُصرِّف بمكان استيراد الحزم منه، ولن نتحدث عن وحدات لغة جو في هذا المقال، لكن سنكتب السطرين التاليين لكي تعمل الشيفرة السابقة، لذا أنشئ الملف go.mod ضمن المجلد cmd كما يلي:
$ nano cmd/go.mod
ثم ضع بداخله التعليمات التالية:
module github.com/gopherguides/cmd replace github.com/gopherguides/creature => ../creature
يخبر السطر الأول المصرِّف أنّ الحزمة cmd
التي أنشأناها هي في الواقع github.com/gopherguides/cmd؛ أما السطر الثاني، فيُخبر المصرِّف أنه يمكن العثور على المجلد github.com/gopherguides/creature محليًا ضمن المجلد creature/..
، لذا احفظ وأغلق الملف ثم أنشئ ملف go.mod في مجلد creature:
$ nano creature/go.mod
أضف السطر التالي من التعليمات البرمجية إلى الملف:
module github.com/gopherguides/creature
يخبر هذا المصرِّف أنّ الحزمة creature
التي أنشأناها هي في الواقع الحزمة github.com/gopherguides/creature، وبدون ذلك لن تعرف الحزمة cmd
مكان استيراد هذه الحزمة. احفظ وأغلق الملف.
يجب أن يكون لديك الآن بنية المجلد التالية:
├── cmd │ ├── go.mod │ └── main.go └── creature ├── go.mod └── creature.go
الآن بعد أن انتهينا من إنشاء ملفات الضبط اللازمة أصبح بالإمكان تشغيل البرنامج من خلال الأمر التالي:
$ go run cmd/main.go
سيكون الخرج كما يلي:
jellyfish squid squid dolphin
حصلنا عند تشغيل هذا البرنامج على أربع قيم وطبعناها، وإذا أعدنا تشغيل هذا البرنامج عدة مرات، فسنلاحظ أننا نحصل دائمًا على الخرج نفسه بدلًا من نتيجة عشوائية كما هو متوقع، وسبب ذلك هو أنّ الحزمة rand
تُنشئ أعدادًا شبه عشوائية تولِّد الخرج نفسه باستمرار من أجل حالة أولية initial state واحدة.
للحصول على عشوائية أكبر، يمكننا إعادة ضبط مفتاح توليد الأعداد seed في الحزمة، أو ضبط مصدر متغير بحيث تختلف الحالة الأولية في كل مرة نُشغّل فيها البرنامج، وفي لغة جو من الشائع استخدام الوقت الحالي على أساس مفتاح توليد في حزمة rand
، وبما أننا نريد من الحزمة creature
التعامل مع دالة عشوائية، افتح هذا الملف:
$ nano creature/creature.go
أضف التعليمات التالية إليه:
package creature import ( "math/rand" "time" ) var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"} func Random() string { rand.Seed(time.Now().UnixNano()) i := rand.Intn(len(creatures)) return creatures[i] }
استوردنا في هذا البرنامج الحزمة time
واستخدمنا الدالة ()Seed
لضبط مفتاح توليد الأعداد seed في الحزمة إلى وقت التنفيذ آنذاك. احفظ وأغلق الملف.
الآن بتشغيل البرنامج:
$ go run cmd/main.go
ستحصل على ما يلي:
jellyfish octopus shark jellyfish
إذا شغلت البرنامج عدة مرات، فستحصل على نتائج مختلفة في كل مرة، وعمومًا هذا ليس نهجًا مثاليًا لأن الدالة ()creature.Random
تُستدعى في كل مرة ولأنه يُعاد ضبط مفتاح توليد الأعداد من خلال الاستدعاء (()rand.Seed(time.Now().UnixNano
، وإعادة ضبط مفتاح التوليد re-seeding باستمرار قد يصادف استعمال قيمتين متماثلتين في حالتنا لو استدعي أكثر من مرة بدون تغير الوقت، أي إذا لم تتغير الساعة الداخلية، وهذا سيؤدي إلى تكرار محتمل للنمط العشوائي أو سيزيد من وقت المعالجة في وحدة المعالجة المركزية عن طريق جعل البرنامج ينتظر حتى تتغير الساعة الداخلية، ولحل هذه المشكلة يمكننا استخدام الدالة ()init
، لذا سنعدّل الملف creature.go:
$ nano creature/creature.go
أضف ما يلي إلى الملف:
package creature import ( "math/rand" "time" ) var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"} func init() { rand.Seed(time.Now().UnixNano()) } func Random() string { i := rand.Intn(len(creatures)) return creatures[i] }
إضافة الدالة ()init
ستخبر المصرِّف أنه يجب استدعاؤها عند استيراد الحزمة creature
ولمرة واحدة فقط، وبالتالي يكون لدينا مفتاح توليد seed واحد لتوليد العدد العشوائي، ويجنبنا هذا النهج تكرار تنفيذ التعليمات البرمجية، والآن إذا أعدنا تشغيل الشيفرة عدة مرات، فسنحصل على نتائج مختلفة:
$ go run cmd/main.go
سيكون الخرج كما يلي:
dolphin squid dolphin octopus
رأينا في هذا القسم كيف يمكّننا استخدام الدالة ()init
من إجراء العمليات الحسابية أو التهيئة المناسبة قبل استخدام الحزمة، وسنرى فيما يلي كيف يمكننا استخدام عدة تعليمات تهيئة ()init
في حزمة ما.
استخدام عدة تعليمات من ()init
يمكن التصريح عن الدالة ()init
أكثر من مرة ضمن الحزمة على عكس الدالة ()main
التي لا يمكن التصريح عنها إلا مرةً واحدةً، وعمومًا يؤدي وجود أكثر من دالة تهيئة إلى حدوث التباس في أولوية التنفيذ، أي مَن يُنفَّذ أولًا ومَن لا يُنفَذ، لذا سيوضح هذا القسم كيفية استخدام تعليمات تهيئة متعددة وكيفية التحكم بها.
ستُنفّذ عادةً دوال init()
بالترتيب نفسه الذي تظهر فيه في الشيفرة مثل ما يلي:
package main import "fmt" func init() { fmt.Println("First init") } func init() { fmt.Println("Second init") } func init() { fmt.Println("Third init") } func init() { fmt.Println("Fourth init") } func main() {}
لنُشغّل البرنامج كما يلي:
$ go run main.go
سيكون الخرج كما يلي:
First init Second init Third init Fourth init
لاحظ أنّ تعليمات التهيئة قد نُفِّذت حسب الترتيب الذي وجدها فيه المصرِّف، وعمومًا قد لا يكون من السهل دائمًا تحديد الترتيب الذي ستُنفّذ فيه تعليمات التهيئة، ففي المثال التالي سنعرض حزمةً أكثر تعقيدًا تتضمن عدة ملفات وكل ملف لديه دالة تهيئة خاصة به. لتوضيح الأمر سننشئ برنامجًا يشارك متغيرًا اسمه message
ويطبعه، والآن احذف مجلدات creature و cmd ومحتوياتهما واستبدلهما بالمجلدات وبنية الملف التالية:
├── cmd │ ├── a.go │ ├── b.go │ └── main.go └── message └── message.go
سنضع الآن محتويات كل ملف في a.go، لذا أضف الأسطر التالية:
package main import ( "fmt" "github.com/gopherguides/message" ) func init() { fmt.Println("a ->", message.Message) }
يتضمن الملف دالة تهيئة واحدة تطبع قيمة message.Message
من الحزمة message
، والآن أضف الأسطر التالية في b.go:
package main import ( "fmt" "github.com/gopherguides/message" ) func init() { message.Message = "Hello" fmt.Println("b ->", message.Message) }
لدينا في هذا الملف دالة تهيئة واحدة تُسند السلسلة Hello
إلى المتغير message.Message
وتطبعها، والآن أنشئ الآن ملف main.go كما يلي:
package main func main() {}
هذا الملف لا يفعل شيئًا، ولكنه يوفر نقطة دخول للبرنامج لكي يُنفّذ، والآن أنشئ ملف message.go:
package message var Message string
تُصرّح حزمة message
عن المتغير المُصدّر Message
، ولتشغيل البرنامج نفّذ الأمر التالي من مجلد cmd:
$ go run *.go
نحتاج إلى إخبار المصرِّف بأنه يجب تصريف جميع ملفات go.
الموجودة في مجلد cmd نظرًا لوجود العديد من ملفات جو في مجلد cmd الذي يُشكّل الحزمة الرئيسية main
، فمن خلال كتابة go.*
يمكننا إخبار المُصرّف أنه عليه تحميل كل الملفات التي لاحقتها go.
في مجلد cmd، وإذا شغّلنا الأمر go run main.go
، فسيفشل البرنامج في التصريف لأنه لن يرى ملفَي a.go و b.go، وسنحصل على الخرج التالي:
a -> b -> Hello
وفقًا لقواعد لغة جو في تهيئة الحزم، فإنه عند مصادفة عدة ملفات في الحزمة نفسها، ستكون أفضلية المعالجة وفق الترتيب الأبجدي alphabetical، لذا في أول مرة طبعنا فيها message.Message
من a.go كانت القيمة المطبوعة هي القيمة الفارغة، وستبقى فارغةً طالما لم تُهيئ من خلال الدالة ()init
في الملف b.go، وإذا أردنا تغيير اسم ملف a.go إلى c.go، فسنحصل على نتيجة مختلفة:
b -> Hello a -> Hello
الآن أصبح المصّرِف يرى الملف b.go أولًا، وبالتالي أصبحت قيمة message.Message
مُهيّئة بالقيمة Hello
وذلك بعد تنفيذ الدالة ()init
في ملف c.go.
لاحظ أنّ هذا السلوك قد يخلق أخطاءً و مشاكل لاحقة لأنه من الشائع تغيير أسماء الملفات أثناء تطوير أيّ مشروع، وهذا يؤثر على ترتيب تنفيذ دالة التهيئة كما أوضحنا.
يفضَّل تقديم الملفات المتعددة الموجودة ضمن الحزمة نفسها ضمن ملف واحد بترتيب مُعجمي lexical order إلى المصرِّف للتحكم بنهج التهيئة المتعدد، وبالتالي ضمان أنّ كل دوال التهيئة تُحمّل من أجل التصريح عنها في ملف واحد، وبالتالي منع تغيير ترتيب تنفيذها حتى لو تغيرت الأسماء.
يجب أن تحاول أيضًا تجنب إدارة الحالة managing state في الحزمة الخاصة بك باستخدام المتغيرات العامة، أي المتغيرات التي يمكن الوصول إليها من أيّ مكان في الحزمة، ففي البرنامج السابق مثلًا كان المتغير message.Message
مُتاحًا في أيّ مكان ضمن الحزمة مع الحفاظ على حالة البرنامج، وبسبب هذه الإمكانية كانت دوال التهيئة قادرةً على الوصول إلى هذا المتغير وتعديل قيمته، وبالتالي أصبح من الصعب التنبؤ بسلوك هذا البرنامج، ولتجنب ذلك يُفضَّل إبقاء المتغيرات ضمن مساحات يمكنك التحكم بها مع إمكانية وصول ضئيلة لها قدر الإمكان حسب حاجة البرنامج.
قد يؤدي وجود عدة تعليمات تهيئة ضمن البرنامج إلى ظهور تأثيرات غير مرغوب فيها ويجعل من الصعب قراءة برنامجك أو التنبؤ به، كما يضمن لك تجنب استخدام تعليمات التهيئة المتعددة أو الاحتفاظ بها جميعًا في ملف واحد بعدم تغير سلوك برنامجك عند نقل الملفات أو تغيير الأسماء.
استخدام دالة التهيئة لتحقيق مفهوم التأثير الجانبي
غالبًا ما تستورَد بعض الحزم في لغة جو بغية الاستفادة من تأثيرها الجانبي فقط وليس من أجل أيّ مكوِّن آخر من مكوناتها، وغالبًا ما يكون ذلك عندما تتضمن هذه الحزمة دالة تهيئة بداخلها، وبالتالي تُنفّذ قبل أيّ شيفرة أخرى بمجرد استيراد الحزمة، وبالتالي إمكانية التلاعب بالحالة التي يبدأ بها البرنامج، وتسمى هذه العملية باستيراد التأثير الجانبي.
إحدى حالات الاستخدام الشائعة لمفهوم استيراد التأثير الجانبي هي الحصول على معلومات أولية تُفيد في تحديد أي جزء من شيفرة ما يجب أن يُنفّذ، ففي حزمة image
مثلًا تحتاج الدالة image.Decode
إلى معرفة تنسيق الصورة التي تحاول فك تشفيرها (jpg ،png ،gif، …إلخ) قبل أن تتمكن من تنفيذها، وهكذا أمر يمكن تحقيقه من خلال مفهوم التأثير الجانبي، فلنقل أنك تحاول استخدام image.Decode
مع ملف بتنسيق png كما يلي:
. . . func decode(reader io.Reader) image.Rectangle { m, _, err := image.Decode(reader) if err != nil { log.Fatal(err) } return m.Bounds() } . . .
هذا البرنامج سليم من ناحية التصريف، لكن إذا حاولت فك تشفير صورة بتنسيق png، فستتلقى خطأً، ولحل هذه المشكلة سنحتاج إلى تسجيل تنسيق الصورة في الدالة image.Decode
، ولحسن الحظ تتضمن الحزمة image/png
تعليمة التهيئة التالية:
func init() { image.RegisterFormat("png", pngHeader, Decode, DecodeConfig) }
وبالتالي إذا استوردنا image/png
إلى الشيفرة السابقة، فستُنفَّذ دالة التهيئة ()image.RegisterFormat
مباشرةً وقبل أيّ شيء آخر.
. . . import _ "image/png" . . . func decode(reader io.Reader) image.Rectangle { m, _, err := image.Decode(reader) if err != nil { log.Fatal(err) } return m.Bounds() }
سيؤدي هذا إلى ضبط حالة البرنامج وتسجيل أننا نحتاج إلى إصدار png من image.Decode()
، وسيحدث هذا التسجيل بوصفه أثرًا جانبيًا لاستيراد الحزمة image/png
.
ربما لاحظت وجود الشرطة السفلية _
قبل "image/png"
، فهذا أمر مهم لأنّ جو لا تسمح لك باستيراد حزمة غير مستخدَمة في البرنامج، وبالتالي فإنّ وجود الشرطة السفلية سيتجاهل أيّ شيء في الحزمة المستورَدة باستثناء تعليمة التهيئة -أي التأثير الجانبي-، وهذا يعني أنه حتى إذا لم نستورِد الحزمة image/png
، فيمكننا استيراد تأثيرها الجانبي.
من المهم أن تعرف متى تحتاج إلى استيراد حزمة للحصول على تأثيرها الجانبي، فبدون التسجيل الصحيح، من المحتمل أن تتمكن جو من تصريف برنامجك ولكن لن يعمل كما تتوقع، وعادةً ما تتضمن مراجع (أو توثيقات documentation) المكتبة القياسية إشارةً إلى الحاجة إلى هكذا نوع من التأثيرات مع حالات معينة، وبالتالي إذا كتبت حزمةً تتطلب استيرادًا للتأثيرات الجانبية، فيجب عليك أيضًا التأكد من توثيق دالة التهيئة التي تستخدِمها حتى يتمكن المستخدِمون الذين يستورِدون الحزمة الخاصة بك من استخدامها بطريقة صحيحة.
الخاتمة
تعلّمنا في هذا المقال استخدام دالة التهيئة ()init
وفهمنا كيف أنها تُنفَّذ قبل أيّ شيء آخر في البرنامج وأنها تستطيع أداء بعض المهام وضبط حالة البرنامج الأولية، كما تحدّثنا أيضًا عن تعليمات التهيئة المتعددة والموجودة ضمن ملفات متعددة وكيف أنّ ترتيب تنفيذها يعتمد على على أمور محددة مثل الترتيب الأبجدي أو المعجمي لملفات الحزمة.
ترجمة -وبتصرُّف- للمقال Understanding init in Go لصاحبه Gopher Guides.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.