اذهب إلى المحتوى

تعرف على دالة التهيئة init واستخدامها في لغة جو Go


هدى جبور

تُستخدَم الدالة المُعرّفة مسبقًا ()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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...