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

هدى جبور

الأعضاء
  • المساهمات

    26
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو هدى جبور

  1. أضاف مؤلفو لغة جو في النسخة 1.13 طريقةً جديدةً لإدارة المكتبات التي يعتمد عليها مشروع مكتوب باستخدام هذه اللغة، تسمى وحدات Go، وقد أتت استجابةً إلى حاجة المطورين إلى تسهيل عملية الحفاظ على الإصدارات المختلفة من الاعتماديات dependencies وإضافة المزيد من المرونة إلى الطريقة التي ينظم بها المطورون مشاريعهم على أجهزتهم. تتكون الوحدات عادةً في لغة جو من مشروع أو مكتبة واحدة إلى جانب مجموعة من حزم لغة جو التي تُطلق released معًا بعد ذلك. تعمل الوحدات في هذه اللغة على حل العديد من المشكلات بالاستعانة بمتغير البيئة GOPATH الخاص بنظام التشغيل، من خلال السماح للمستخدمين بوضع شيفرة مشروعهم في المجلد الذي يختارونه وتحديد إصدارات الاعتماديات لكل وحدة. سننشئ في هذه المقالة وحدة جو عامة public خاصة بنا وسنضيف إليها حزمة، وسنتعلم أيضًا كيفية إضافة وحدة عامة أنشأها آخرون إلى مشروعنا، وإضافة إصدار محدد من هذه الوحدة إلى المشروع. المتطلبات أن يكون لديك مساحة عمل خاصة في لغة جو، وإذا لم يكن لديك اتبع سلسلة المقالات التالية: تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu. تثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك أو اس macOS. تثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز. معرفة بكيفية إنشاء الحزم في لغة جو Go. إنشاء وحدة جديدة قد تبدو الوحدة module مشابهة للحزمة package للوهلة الأولى في لغة جو، لكن تحتوي الوحدة على عدد من الشيفرات التي تُحقق وظيفة الحزمة، إضافةً إلى ملفين مهمين في المجلد الجذر root هما "go.mod" و "go.sum". تحتوي هذه الملفات على معلومات تستخدمها أداة go لتتبع إعدادات ضبط Configuration الوحدة الخاصة بك، وتجري عادةً صيانتها بواسطة الأداة نفسها، لذا لن تكون مضطرًا لفعل ذلك بنفسك. أول شيء عليك التفكير فيه هو المكان الذي ستضع فيه الوحدة، فمفهوم الوحدات يجعل بالإمكان وضع المشاريع في أي مكان من نظام الملفات، وليس فقط ضمن مجلد حُدد مسبقًا في جو. ربما يكون لديك مجلد خاص لإنشاء المشاريع وترغب في استخدامه، لكن هنا سننشئ مجلدًا باسم projects، ونسمي الوحدة الجديدة mymodule. يمكنك إنشاء المجلد projects إما من خلال بيئة تطوير متكاملة IDE أو من خلال سطر الأوامر. في حال استخدامك لسطر الأوامر، ابدأ بإنشاء المجلد وانتقل إليه: $ mkdir projects $ cd projects سننشئ بعد ذلك مجلد الوحدة، إذ يكون عادةً اسم المجلد المتواجد في المستوى الأعلى من الوحدة هو نفس اسم الوحدة لتسهيل تتبع الأشياء. أنشئ ضمن مجلد projects مجلدًا باسم "mymodule": $ mkdir mymodule بعد إنشاء مجلد الوحدة، ستصبح لدينا البنية التالية: └── projects └── mymodule الخطوة التالية هي إنشاء ملف "go.mod" داخل مجلد "mymodule" كي نُعّرف الوحدة، وسنستخدم لأجل ذلك الأمر go mod init ونحدد له اسم الوحدة mymodule كما يلي: $ go mod init mymodule يعطي هذا الأمر الخرج التالي عند إنشاء الوحدة: go: creating new go.mod: module mymodule سيصبح لديك الآن بنية المجلد التالية: └── projects └── mymodule └── go.mod دعنا نلقي نظرةً على ملف "go.mod" لمعرفة ما فعله الأمر go mod init. الملف go.mod يلعب الملف "go.mod" جزءًا مهمًا جدًا عند تشغيل الأوامر باستخدام الأداة go، إذ يحتوي على اسم الوحدة وإصدارات الوحدات الأخرى التي تعتمد عليها الوحدة الخاصة بك، ويمكن أن يحتوي أيضًا على موجّهات directives أخرى مثل replace، التي يمكن أن تكون مفيدة لإجراء عمليات تطوير على وحدات متعددة في وقت واحد. افتح الملف go.mod الموجود ضمن المجلد mymodule باستخدام محرر نانو nano أو محرر النصوص المفضل لديك: $ nano go.mod ستكون محتوياته هكذا (قليلة أليس كذلك؟): module mymodule go 1.16 في السطر الأول لدينا موجّه يُدعى module، مهمته إخبار مُصرّف اللغة عن اسم الوحدة الخاصة بك، بحيث عندما يبحث في مسارات الاستيراد import ضمن حزمة فإنه يعرف أين عليه أن يبحث عن mymodule. تأتي القيمة mymodule من المعامل الذي مررته إلى go mod init: module mymodule لدينا في السطر الثاني والأخير من الملف موجّهًا آخرًا هو go، مهمته إخبار المصرّف عن إصدار اللغة التي تستهدفها الوحدة. بما أننا ننشئ الوحدة باستخدام النسخة 1.16 من لغة جو، فإننا نكتب: go 1.16 سيتوسع هذا الملف مع إضافة المزيد من المعلومات إلى الوحدة، ولكن من الجيد إلقاء نظرة عليه الآن لمعرفة كيف يتغير عند إضافة اعتماديات في أوقات لاحقة. لقد أنشأت الآن وحدة جو باستخدام go mod init واطلعت على ما يحتويه ملف "go.mod"، لكن ليس لهذه الوحدة أي وظيفة تفعلها حتى الآن. سنبدأ في الخطوة التالية بتطوير هذه الوحدة وإضافة بعض الوظائف إليها. كيفية إضافة شيفرات برمجية إلى الوحدة ستنشئ ملف "main.go" داخل المجلد "mymodule" لنضع فيه شيفرة برمجية ونشغلها ونختبر صحة عمل الوحدة. يُعد استخدام ملف "main.go" في برامج هذه اللغة للإشارة إلى نقطة بداية البرنامج أمرًا شائعًا. الأهم من هذا الملف هو الدالة main التي سنكتبها بداخله، أما اسم الملف بحد ذاته فهو اختياري، لكن من الأفضل تسميته بهذا الاسم لجعل عملية العثور عليه أسهل. سنجعل الدالة main تطبع رسالةً ترحيبيةً عند تشغيل البرنامج. نُنشئ الآن ملفًا باسم "main.go" باستخدام محرّر نانو أو أي محرر نصوص مفضل لديك: $ nano main.go ضع التعليمات البرمجية التالية في ملف "main.go"، إذ تصرّح هذه التعليمات أولًا عن الحزمة، ثم تستورد الحزمة fmt، وتطبع رسالةً ترحيبية: package main import "fmt" func main() { fmt.Println("Hello, Modules!") } تُمثّل الحزمة في لغة جو بالمجلد الذي تُنشأ فيه، ولكل ملف داخل مجلد الحزمة سطر يُصرّح فيه عن الحزمة التي ينتمي إليها هذا الملف. أعطينا الحزمة الاسم main في ملف "main.go" الذي أنشأته للتو، ويمكنك تسمية الحزمة بالطريقة التي تريدها، ولكن حزمة main خاصة في لغة جو، فعندما يرى مُصرّف اللغة أن الحزمة تحمل اسم main، فإنه يعلم أن الحزمة ثنائية binary، ويجب تصريفها إلى ملف تنفيذي، وليست مجرد مكتبة مصممة لاستخدامها في برنامج آخر. بعد تحديد الحزمة التي يتبع لها الملف، يجب أن نستورد الحزمة fmt لكي نستطيع استخدام الدالة Println منها، وذلك لطباعة الرسالة الترحيبية !Hello, Modules على شاشة الخرج. أخيرًا، نُعرّف الدالة main التي بدورها لها معنى خاص في لغة جو فهي مرتبطة بحزمة main، وعندما يرى المُصرّف دالة اسمها main داخل حزمة باسم mainسيعرف أن الدالة main هي الدالة الأولى التي يجب تشغيلها، ويُعرف هذا بنقطة دخول البرنامج. بمجرد إنشاء ملف "main.go"، ستصبح بنية مجلد الوحدة مشابهة لما يلي: └── projects └── mymodule └── go.mod └── main.go إذا كنت معتادًا على استخدام لغة جو ومتغير البيئة GOPATH، فإن تشغيل شيفرة ضمن وحدة مشابهٌ لكيفية تشغيل شيفرة من مجلد في GOPATH. لا تقلق إذا لم تكن معتادًا على GOPATH، لأن استخدام الوحدات يحل محل استخدامها. هناك طريقتان شائعتان لتشغيل برنامج تنفيذي في لغة جو، هما: إنشاء ملف ثنائي باستخدام go build وتشغيله، أو تشغيل الملف مباشرةً باستخدام go run. ستستخدم هنا go run لتشغيل الوحدة مباشرةً، إذ أن الأخير يجب أن يُشغّل لوحده. شغّل ملف "main.go" الذي أنشأته باستخدام الأمر go run: $ go run main.go ستحصل على الخرج التالي: Hello, Modules! أضفنا في هذا القسم الملف "main.go" إلى الوحدة التي أنشأناها وعرّفنا فيها نقطة دخول متمثلة بالدالة main وجعلناها تطبع عبارة ترحيبية. لم نستفد حتى الآن من خصائص أو فوائد الوحدات في لغة جو؛ إذ لا تختلف الوحدة التي أنشأناها للتو عن أي ملف يُشغّل باستخدام go run، وأول فائدة حقيقية لوحدات جو هي القدرة على إضافة الاعتماديات إلى مشروعك في أي مجلد وليس فقط ضمن مجلد "GOPATH"، كما يمكنك أيضًا إضافة حزم إلى وحدتك. سنوسّع في القسم التالي هذه الوحدة عن طريق إنشاء حزمة إضافية داخلها. إضافة حزمة إلى الوحدة قد تحتوي الوحدة على أي عدد من الحزم والحزم الفرعية sub-packages على غرار حزمة جو القياسية، وقد لا تحتوي على أية حزم. سننشئ الآن حزمة باسم mypackage داخل المجلد "mymodule". سنستخدم كما جرت العادة الأمر mkdir ونمرر له الوسيط mypackage لإنشاء مجلد جديد داخل المجلد "mymodule": $ mkdir mypackage سيؤدي هذا إلى إنشاء مجلد جديد باسم "mypackage" مثل حزمة فرعية جديدة من المجلد "mymodule": └── projects └── mymodule └── mypackage └── main.go └── go.mod استخدم الأمر cd للانتقال إلى المجلد "mypackage" الجديد، ثم استخدم محرر نانو أو محرر النصوص المفضل لديك لإنشاء ملف "mypackage.go". يمكن أن يكون لهذا الملف أي اسم، ولكن استخدم نفس اسم الحزمة لتسهيل عملية العثور على الملف الأساسي للحزمة: $ cd mypackage $ nano mypackage.go ضِف داخل الملف "mypackage.go" الشيفرة التالية التي تتضمن دالةً تدعى PrintHello تطبع عبارة !Hello, Modules! This is mypackage speaking عند استدعائها. package mypackage import "fmt" func PrintHello() { fmt.Println("Hello, Modules! This is mypackage speaking!") } نحن نريد أن تكون الدالة PrintHello متاحةً للحزم والبرامج الأخرى، لذا جعلناها دالةً مُصدّرةً exported function بكتابتنا لأول حرف منها بالحالة الكبيرة P. بعد أن أنشأنا الحزمة mypackage ووضعنا فيها دالةً مُصدَّرةً، سنحتاج الآن إلى استيرادها من حزمة mymodule لاستخدامها. هذا مشابه لكيفية استيراد حزم أخرى مثل حزمة fmt، لكن هنا يجب أن نضع اسم الوحدة قبل اسم الحزمة mymodule/mypackage. افتح ملف "main.go" من مجلد "mymodule" واستدعِ الدالة PrintHello كما يلي: package main import ( "fmt" "mymodule/mypackage" ) func main() { fmt.Println("Hello, Modules!") mypackage.PrintHello() } كما ذكرنا قبل قليل، عندما نريد استيراد الحزمة mypackage نضع اسم الوحدة قبلها مع وضع الفاصل / بينهما، وهو نفسه اسم الوحدة الذي وضعته في ملف "go.mod" (أي mymodule). "mymodule/mypackage" إذا أضفت لاحقًا حزمًا أخرى داخل mypackage، يمكنك استيرادها بطريقة مماثلة. على سبيل المثال، إذا كان لديك حزمة أخرى تسمى extrapackage داخل mypackage، فسيكون مسار استيراد هذه الحزمة هو mymodule/mypackage/extrapackage. شغّل الوحدة مرةً أخرى بعد إجراء التعديلات عليها باستخدام الأمر go run ومرر اسم الملف "main.go" الموجود ضمن المجلد "mymodule" كما في السابق: $ go run main.go عند التشغيل سترى نفس الرسالة التي حصلنا عليها سابقًا والمتمثلة بالرسالة !Hello, Modules إضافةً إلى الرسالة المطبوعة من دالة PrintHello والموجودة في الحزمة الجديدة التي أضفناها: Hello, Modules! Hello, Modules! This is mypackage speaking! لقد أضفت الآن حزمةً جديدة إلى وحدتك عن طريق إنشاء مجلد يسمى "mypackage" مع دالة PrintHello. يمكنك لاحقًا توسيع هذه الوحدة بإضافة حزم ودوال جديدة إليها، كما سيكون من الجيد تضمين وحدات أنشأها أشخاص آخرون في وحدتك. سنضيف في القسم التالي وحدةً بعيدة (من غيت Git) مثل اعتمادية لوحدتك. تضمين وحدة بعيدة أنشأها آخرون في وحدتك تُوزَّع وحدات لغة جو من مستودعات التحكم بالإصدار Version Control Repositories -أو اختصارًا VCSs- وأكثرها شيوعًا غيت git. عندما ترغب بإضافة وحدة جديدة مثل اعتمادية لوحدتك، تستخدم مسار المستودع للإشارة إلى الوحدة التي ترغب باستخدامها. عندما يرى مُصرّف اللغة مسار الاستيراد لهذه الوحدة، سيكون قادرًا على استنتاج مكان وجود هذه الوحدة اعتمادًا على مسار المستودع. سنضيف في المثال التالي المكتبة cobra مثل اعتمادية للوحدة التي أنشأناها، وهي مكتبة شائعة لإنشاء تطبيقات الطرفية Console. بطريقة مشابهة لما فعلناه عند إنشاء الوحدة mymodule سنستخدم الأداة go مرةً أخرى، لكن سنضيف لها get أي سنستخدم الأمر go get، وسنضع بعده مسار المستودع. شغّل الأمر go get من داخل مجلد "mymodule": $ go get github.com/spf13/cobra عند تشغيل هذا الأمر ستبحث أداة go عن مستودع Cobra من المسار الذي حددته، وستبحث عن أحدث إصدار منها من خلال البحث في الفروع ووسوم المستودع. بعد ذلك، سيُحمّل هذا الإصدار ويبدأ في تعقبها، وذلك من خلال إضافة اسم الوحدة والإصدار إلى ملف "go.mod" للرجوع إليه مستقبلًا. افتح ملف "go.mod" الموجود في المجلد "mymodule" لترى كيف حدّثت أداة go ملف "go.mod" عند إضافة الاعتمادية الجديدة. قد يتغير المثال أدناه اعتمادًا على الإصدار الحالي من Cobra أو إصدار أداة go التي تستخدمها، ولكن يجب أن تكون البنية العامة للتغييرات متشابهة: module mymodule go 1.16 require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/spf13/cobra v1.2.1 // indirect github.com/spf13/pflag v1.0.5 // indirect ) لاحظ إضافة قسم جديد مُتمثّل بالموجّه require، ومهمته إخبار مُصرّف اللغة عن الوحدة التي تريدها (في حالتنا هي github.com/spf13/cobra) وعن إصدارها أيضًا. نلاحظ أيضًا أن هناك تعليق مكتوب فيه indirect، ليوضح أنه في الوقت الذي أُضيف فيه الموجّه require، لم تكن هناك إشارة مباشرة إلى الوحدة في أي من ملفات المصدر الخاصة بالوحدة. نلاحظ أيضًا وجود بعض الأسطر في require تشير إلى وحدات أخرى تتطلبها المكتبة Cobra والتي يجب على أداة جو أن تشير إليها أيضًا. نلاحظ أيضًا أنه عند تشغيل الأمر go run أُنشئ ملف جديد باسم "go.sum" في مجلد "mymodule"، وهو ملفٌ آخر مهم لوحدات جو يحتوي على معلومات تستخدمها جو لتسجيل قيم معمّاة hashes وإصدارات معينة من الاعتماديات. يضمن هذا تناسق الاعتماديات، حتى لو ثُبتت على جهاز مختلف. ستحتاج أيضًا إلى تحديث الملف "main.go" بعد تنزيل الاعتمادية، وذلك من خلال إضافة بعض التعليمات البرمجية الخاصة بها، لكي تتمكن من استخدامها. افتح الملف "main.go" الموجود في المجلد "mymodule" وضع فيه المحتويات التالية: package main import ( "fmt" "github.com/spf13/cobra" "mymodule/mypackage" ) func main() { cmd := &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { fmt.Println("Hello, Modules!") mypackage.PrintHello() }, } fmt.Println("Calling cmd.Execute()!") cmd.Execute() } تُنشئ هذه الشيفرة بنية cobra.Command مع دالة Run تطبع عبارة ترحيب، والتي ستُنفّذ بعد ذلك باستدعاء ()cmd.Execute. شغّل الشيفرة الآن بعد أن عدلناها: $ go run main.go سترى الخرج التالي الذي يبدو مشابهًا لما رأيته من قبل، لكننا استخدمنا هنا الاعتمادية الجديدة التي أضفناها كما هو موضح في السطر الأول: Calling cmd.Execute()! Hello, Modules! Hello, Modules! This is mypackage speaking! يؤدي استخدام الأمر go get لإضافة اعتماديات إلى وحدتك إلى تنزيل أحدث إصدار من هذه الاعتمادية، وهو أمرٌ جيد لأن أحدث إصدار يتضمن كل التعديلات الجديدة والإصلاحات. قد نرغب أحيانًا في استخدام إصدار أقدم أو فرع مُحدد من مستودع هذه الاعتمادية. سنستخدم في القسم التالي الأداة go get لمعالجة هذه الحالة. استخدام إصدار محدد من وحدة بما أن وحدات لغة جو توزّع من مستودع التحكم في الإصدار، بالتالي يمكنها استخدام ميزاته، مثل الوسوم والفروع والإيداعات. يمكنك الإشارة إلى هذه الأمور في اعتمادياتك باستخدام الرمز @ في نهاية مسار الوحدة جنبًا إلى جنب مع الإصدار الذي ترغب في استخدامه. لاحظ أنه كان بإمكانك استخدام هذه الميزة مباشرةً في المثال السابق، فهي لا تتطلب شيئًا إلا وضع الرمز والإصدار. إذًا يمكننا استنتاج أنه في حال عدم استخدام هذا الرمز، يفترض جو أننا نريد أحدث إصدار، وهذا يُقابل وضع latest بعد هذا الرمز. latest هي ميزة خاصة لأداة جو، وليست جزءًا من الوحدة أو مستودع الوحدة التي تريد استخدامها مثل my-tag أو my-branch. إذًا، تُكافئ الصيغة التالية تنزيل أحدث إصدار من الوحدة سالفة الذكر: $ go get github.com/spf13/cobra@latest تخيل الآن أن هناك وحدة تستخدمها، وهي قيد التطوير حاليًا، ولنفترض اسمها your_domain/sammy/awesome. افترض أن هناك ميزة أضيفت إلى هذه الوحدة في فرع اسمه new-feature. لإضافة هذا الفرع مثل اعتمادية للوحدة الخاصة بك، يمكنك ببساطة استخدام مسار الوحدة متبوعًا بالرمز @ متبوعًا باسم الفرع: $ go get your_domain/sammy/awesome@new-feature عند تشغيل هذا الأمر ستتصل أداة go بالمستودع your_domain/sammy/awesome، وستنزّل الفرع new-features من آخر إيداع، وتضيف هذه المعلومات إلى الملف "go.mod". ليست الفروع الطريقة الوحيدة التي يمكنك من خلالها استخدام الرمز @، ولكن يمكنك استخدامه مع الوسوم أو الإيداعات أيضًا، فقد يكون أحيانًا أحدث إصدار من المكتبة التي تستخدمها إيداعًأ معطلًا، وفي هذه الحالة يمكنك الرجوع إلى إيداع سابق واستخدامه. بالعودة إلى نفس المثال السابق، افترض أنك بحاجة إلى الإشارة إلى الإيداع 07445ea من github.com/spf13/cobra لأنه يحتوي على بعض التغييرات التي تحتاجها ولا يمكنك استخدام إصدار آخر لسبب ما. في هذه الحالة، يمكنك وضع قيمة معمّاة hash بعد الرمز @. شغّل الآن الأمر go get من داخل مجلد "mymodule" لتنزيل الإصدار الجديد: $ go get github.com/spf13/cobra@07445ea إذا فتحت الآن ملف "go.mod" الخاص بالوحدة، فسترى أن go get قد حدّث سطر require للإشارة إلى الإيداع الذي تستخدمه: module mymodule go 1.16 require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/spf13/cobra v1.1.2-0.20210209210842-07445ea179fc // indirect github.com/spf13/pflag v1.0.5 // indirect ) على عكس الوسوم أو الفروع، ونظرًا لأن الإيداع يمثّل نقطةً زمنيةً معينة، تتضمن جو معلومات إضافية في موجّه require للتأكد من استخدام الإصدار الصحيح مستقبلًا؛ فإذا نظرت إلى الإصدار، سترى أنه يتضمن إيداعًا معمّى hash قد أضفته، أي v1.1.2-0.20210209210842-07445ea179fc. تستخدم جو هذه الوظيفة لدعم إطلاق عدة إصدارات من الوحدة، فعندما تطلق وحدة جو إصدارًا جديدًا، سيُضاف وسمًا جديدًا إلى المستودع مع وسم لرقم الإصدار. إذا كنت تريد استخدام إصدار محدد، يمكنك النظر إلى قائمة الوسوم في المستودع حتى تجد الإصدار الذي تبحث عنه، أما إذا كنت تعرف الإصدار، لن تكون مضطرًا للبحث ضمن قائمة الوسوم لأنها مرتبة بانتظام. بالعودة إلى نفس المثال Cobra السابق، ولنفترض أنك بحاجة إلى استخدام الإصدار 1.1.1، الذي يملك وسمًا يدعى v1.1.1 في مستودع Cobra. سنستخدم الأمر go get مع الرمز @ حتى نتمكن من استخدام هذا الإصدار الموسوم تمامًا كما فعلنا مع الفروع أو الوسوم التي لا تشير لإصدار non-version tag. الآن، حدّث الوحدة الخاصة بك حتى تستخدم Cobra 1.1.1 من خلال تشغيل الأمر go get مع رقم الإصدار v1.1.1: $ go get github.com/spf13/cobra@v1.1.1 إذا فتحت الآن ملف "go.mod" الخاص بالوحدة، فسترى أن go get قد حدّث سطرrequire للإشارة إلى الإيداع الذي تستخدمه: module mymodule go 1.16 require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/spf13/cobra v1.1.1 // indirect github.com/spf13/pflag v1.0.5 // indirect ) أخيرًا، إذا كنت تستخدم إصدارًا محددًا من المكتبة، مثل الإيداع 07445ea أو الإصدار v1.1.1 الذين تعرفنا عليهما منذ قليل، ثم قررت استخدام أحدث إصدار من المكتبة، فمن الممكن إنجاز ذلك باستخدام latest كما ذكرنا سابقًا. لتحديث الوحدة الخاصة بك إلى أحدث إصدار من Cobra، استخدم الأمر go get مرةً أخرى مع مسار الوحدة وتحديد الإصدار latest بعد الرمز @: $ go get github.com/spf13/cobra@latest بعد تنفيذ هذا الأمر، يُحدّث ملف "go.mod" ليبدو كما كان عليه عند تنزيل المكتبة Cobra أول مرة. قد يبدو الخرج مختلفًا قليلًا بحسب إصدار جو الذي تستخدمه والإصدار الأخير من Cobra ولكن يُفترض أن تلاحظ أن السطر github.com/spf13/cobra في قسم require قد تحدّث إلى آخر إصدار: module mymodule go 1.16 require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/spf13/cobra v1.2.1 // indirect github.com/spf13/pflag v1.0.5 // indirect ) يُعد الأمر go get أداةً قوية يمكنك استخدامها لإدارة الاعتماديات في ملف "go.mod" دون الحاجة إلى تحريره يدويًا. يتيح لك استخدام الرمز @ مع اسم الوحدة، أن تستخدم إصدارات معينة لوحدة ما، أو حتى إيداعات، أو فروع، أو الرجوع إلى أحدث إصدار من اعتمادياتك كما رأينا للتو. سيسمح استخدام مجموعة الأدوات التي تعرفنا عليها في هذه المقالة بضمان استقرار البرنامج مستقبلًا. الخاتمة أنشأنا في هذه المقالة وحدةً برمجية باستخدام لغة جو مع حزمة فرعية، واستخدمنا تلك الحزمة داخل الوحدة التي أنشأناها. أضفنا أيضًا وحدةً خارجية إليها مثل اعتمادية، وتعلمنا كيفية الإشارة إلى إصدارات الوحدة بطرق مختلفة. ترجمة -وبتصرف- للمقال How to Use Go Modules لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: كيفية استخدام الحزمة Flag في لغة جو Go كتابة أول برنامج (ومكتبة) لك باستخدام لغة البرمجة Go استيراد الحزم في لغة جو Go بناء البرامج المكتوبة بلغة جو Go وتثبيتها
  2. نادرًا ما تكون الأدوات المساعدة لسطر الأوامر Command-line utilities مفيدةً دون ضبط configuration إضافي وذلك عندما يتطلب الأمر إجراء عمليات خارج الصندوق. تُعد الإعدادات الافتراضية للأوامر أمرًا جيدًا ومهمًا، لكن ينبغي أن تتميز بإمكانية قبول إعدادات ضبط محددة من المستخدمين أيضًا. في معظم أنظمة التشغيل يمكن تخصيص أوامر سطر الأوامر من خلال استخدام الرايات Flags؛ وهي سلاسل نصية تُضاف إلى أمر ما، بحيث تؤدي إلى سلوك خاص لهذا الأمر حسب قيمتها. تتيح لك لغة جو إنشاء أدوات مساعدة لسطر الأوامر تقبل رايات يمكن من خلالها تخصيص سلوك الأوامر، وذلك باستخدام حزمة flag من المكتبة القياسية. ستتعلم في هذه المقالة طرقًا مختلفة لاستخدام حزمة flag بهدف إنشاء أنواع مختلفة من الأدوات المساعدة لسطر الأوامر. سنستخدم رايةً للتحكم في خرج برنامج وتقديم وسطاء موضعية Positional arguments (وهي وسطاء يجب وضعها في الموضع أو الترتيب المناسب المُحدد مسبقًا)، إذ يمكننا مزج الرايات والبيانات الأخرى وتنفيذ أوامر فرعية. استخدام الرايات لتغيير سلوك البرنامج يتضمن استخدام حزمة الراية ثلاث خطوات: تبدأ بتعريف متغيرات تلتقط قيم الرايات، ثم تحديد الرايات التي سيستخدمها التطبيق، وتنتهي بتحليل الرايات المُقدمة للتطبيق عند التنفيذ. تُركّز معظم الدوال داخل حزمة الراية على تعريف رايات وربطها بالمتغيرات التي تُعرّفها، وتنجز الدالة ()Parse مرحلة التحليل. لتوضيح الأمور سنُنشئ برنامجًا بسيطًا يتضمن رايةً بوليانية تُغيّر الرسالة المطبوعة؛ فإذا كانت الراية color- موجودة، سيطبع البرنامج الرسالة باللون الأزرق؛ وإذا لم تُقدّم أي راية، فلن يكون للرسالة أي لون. انشئ ملفًا باسم boolean.go: $ nano boolean.go اضِف ما يلي إلى الملف: package main import ( "flag" "fmt" ) type Color string const ( ColorBlack Color = "\u001b[30m" ColorRed = "\u001b[31m" ColorGreen = "\u001b[32m" ColorYellow = "\u001b[33m" ColorBlue = "\u001b[34m" ColorReset = "\u001b[0m" ) func colorize(color Color, message string) { fmt.Println(string(color), message, string(ColorReset)) } func main() { useColor := flag.Bool("color", false, "display colorized output") flag.Parse() if *useColor { colorize(ColorBlue, "Hello, DigitalOcean!") return } fmt.Println("Hello, DigitalOcean!") } يستخدم هذا المثال سلاسل الهروب آنسي ANSI Escape Sequences لجعل الطرفية تُعطي خرجًا ملونًأ؛ وهي سلاسل خاصة من المحارف، لذا من المنطقي أن نُعرّف نوع خاص بها. في مثالنا نسمي هذا النوع Color ونعرّفه على أنه string، ثم نُعرّف لوحة ألوان لاستخدامها بكتلة نسميها const تتضمن عدة خيارات لونية اعتمادًا على النوع السابق. تستقبل الدالة colorize المُعرّفة بعد الكتلة const قيمةً لونيةً من اللوحة السابقة (أي عمليًّا متغير من النوع Color) إضافةً إلى الرسالة المطلوب تلوينها. بعد ذلك توجّه الطرفية Terminal لتغيير اللون عن طريق طباعة تسلسل الهروب للون المطلوب، ثم طباعة الرسالة. أخيرًا، يُطلب من الطرفية إعادة ضبط اللون الأساسي لها من خلال طباعة ColorReset، أي نطبع سلسلة الهروب للون المطلوب ثم رسالتنا، فتظهر باللون المطلوب، ثم نعيد ضبط اللون إلى حالته الأصلي. نستخدم داخل الدالة main الدالة flag.Bool لتعريف راية بوليانية اسمها color. المعامل الثاني لهذه الدالة false هو القيمة الافتراضية للراية، أي عندما نُشغل البرنامج بدونها. على عكس ما قد تتوقعه، فإن ضبط هذا المعامل على true هو أمر غير صحيح، لأننا لا نريد أن يُطبق سلوك التلوين إلا عندما نُمرر الراية كما سترى بعد قليل. إذًا، تكون قيمة هذا المعامل تكون غالبًا false مع الرايات البوليانية. المعامل الأخير هو نص توضيحي لهذه الراية، أي كأنه توثيق استخدام أو وصف. القيمة التي تعيدها الدالة هي مؤشر إلى bool، وتُضبط قيمة هذا المؤشر من خلال الدالة flag.Parse بناءً على الراية التي يُمررها المستخدم. يمكننا بعد ذلك التحقق من قيمة هذا المؤشر البولياني عن طريق تحصيل قيمته باستخدام المعامل *. إذًا باستخدام هذه القيمة المنطقية، يمكننا استدعاء colorize عند تمرير color- أو استدعاء دالة الطباعة العادية fmt.Println دون تلوين إذا لم تُمرر الراية. احفظ وأغلق الملف وشغله بدون تقديم أي راية: $ go run boolean.go ستحصل على الخرج التالي: Hello, DigitalOcean! أعد تشغيله مع تمرير الراية color-: $ go run boolean.go -color سيكون الناتج هو نفس النص، ولكن هذه المرة باللون الأزرق. يمكنك أيضًا إرسال أسماء ملفات أو بيانات أخرى إلى البرنامج، وليس فقط رايات. التعامل مع الوسطاء الموضعية تمتلك الأوامر غالبًا بعض الوسطاء التي تحدد سلوكها. لنأخذ مثلًا الأمر head الذي يطبع الأسطر الأولى من ملف ما. غالبًا ما يُستدعى هذا الأمر بالشكل التالي head example.txt، إذ يُمثل الملف example.txt وسيطًا موضعيًّا هنا. تستمر الدالة ()Parse في تحليل الرايات التي تصادفها ريثما تجد شيئًا آخر لا يُمثّل راية (سنرى بعد قليل أنها ستتوقف عند مصادفة وسيط موضعي). لذا سنتعرف الآن على دالة أخرى توفرها الحزمة flag تتمثل بالدالة ()Args والدالة ()Arg. لتوضيح ذلك سنعيد تنفيذ الأمر head الذي يعرض أول عدة أسطر من ملف معين. أنشئ ملفًا جديدًا يسمى head.go وضِف الشيفرة التالية: package main import ( "bufio" "flag" "fmt" "io" "os" ) func main() { var count int flag.IntVar(&count, "n", 5, "number of lines to read from the file") flag.Parse() var in io.Reader if filename := flag.Arg(0); filename != "" { f, err := os.Open(filename) if err != nil { fmt.Println("error opening file: err:", err) os.Exit(1) } defer f.Close() in = f } else { in = os.Stdin } buf := bufio.NewScanner(in) for i := 0; i < count; i++ { if !buf.Scan() { break } fmt.Println(buf.Text()) } if err := buf.Err(); err != nil { fmt.Fprintln(os.Stderr, "error reading: err:", err) } } نُعرّف بدايةً المتغير count الذي سيُخزّن عدد الأسطر التي يجب أن يقرأها البرنامج من الملف، كما نُعرّف أيضًا الراية n- باستخدام flag.IntVar لكي نعكس سلوك البرنامج head. تسمح لنا هذه الدالة بتمرير المؤشر الخاص بنا إلى متغير على عكس دوال حزمة الراية flag الأخرى، التي لا تحتوي على اللاحقة Var. بغض النظر عن هذا الفرق، ستكون بقية المعاملات للدالة flag.IntVar مثل نظيراتها في دوال flag.Int الأخرى التي لا تتضمن هذه اللاحقة، أي ستكون المعاملات كما يلي: اسم الراية ثم القيمة الافتراضية ثم الوصف. بعد ذلك، نستدعي دالة ()flag.Parse لتفسير دخل المستخدم. يقرأ القسم التالي الملف، إذ نعرّف متغيرًا باسم io.Reader الذي سيُضبط إما على الملف الذي يطلبه المستخدم، أو الدخل القياسي الذي يُمرر إلى البرنامج. نستخدم الدالة flag.Arg داخل التعليمة if للوصول إلى أول وسيط موضعي يأتي بعد الرايات. ستكون قيمة filename، إما اسم ملف يُقدمه المستخدم أو سلسلة فارغة ""؛ ففي حال تقديم اسم ملف، تُستخدم الدالة os.Open لفتح الملف وضبط المتغير io.Reader سالف الذكر؛ وإذا لم يُقدم اسم ملف (أي سلسلة فارغة) فإننا نستخدم os.Stdin للقراءة من الدخل القياسي. يستخدم القسم الأخير bufio.Scanner* الذي أنشئ باستخدام bufio.NewScanner لقراءة الأسطر من متغير io.Reader الذي يُمثله in. بعد ذلك، نكرّر حلقة عدة مرات حسب قيمة count. تُستدعى break إذا كان ناتج قراءة سطر باستخدام buf.Scan هو القيمة false، إذ يشير ذلك إلى أن عدد الأسطر أقل من الرقم الذي يطلبه المستخدم. شغّل هذا البرنامج واعرض محتويات الملف البرمجي نفسه الذي كتبته للتو من خلال استخدام head.go مثل وسيط، أي سنشغّل البرنامج head.go ونقرأ محتوياته أيضًا: $ go run head.go -- head.go الفاصل -- هو راية، إذ يُفسَّر وجودها من قِبل حزمة الراية flag على أنه لن يكون هناك رايات بدءًا منها. سيكون الخرج كما يلي: package main import ( "bufio" "flag" دعنا نستخدم الراية n- التي عرّفناها لتحديد عدد الأسطر المقروءة: $ go run head.go -n 1 head.go سيكون الخرج هو أول سطر فقط من الملف: package main أخيرًا، عندما يكتشف البرنامج أنه لم تُقدّم أية وسطاء موضعية، سيقرأ من الدخل القياسي، تمامًا مثل الأمر head. جرّب تشغيل هذا الأمر: $ echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3 سيكون الخرج على النحو التالي: fish lobsters sharks يقتصر سلوك دوال حزمة الراية التي رأيناها حتى الآن على فحص الأمر المُستدعى كاملًا. لا نريد دائمًا هذا السلوك، خاصةً إذا كانت الأداة التي نريدها تتضمن أوامر فرعية Sub-commands. استخدام FlagSet لدعم إمكانية تحقيق الأوامر الفرعية تتضمن تطبيقات سطر الأوامر الحديثة غالبًا وجود أوامر فرعية، بهدف تجميع مجموعة من الأدوات تحت أمر واحد. الأداة الأكثر شهرة التي تستخدم هذا النمط هي git، فمثلًا في git init، لدينا git هو الأمر الأساسي و init هو الأمر الفرعي التابع له. إحدى السمات البارزة للأوامر الفرعية هي أن كل أمر فرعي يمكن أن يكون له مجموعته الخاصة من الرايات. يمكن لتطبيقات لغة جو أن تدعم الأوامر الفرعية من خلال نوع خاص يُدعى (flag.(*FlagSet. سننشئ برنامجًا يُنفذ أمرًا باستخدام أمرين فرعيين وبرايات مختلفة، وذلك لكي نجعل الأمور واضحة. نُنشئ ملفًا جديدًا يسمى subcommand.go بالمحتويات التالية: package main import ( "errors" "flag" "fmt" "os" ) func NewGreetCommand() *GreetCommand { gc := &GreetCommand{ fs: flag.NewFlagSet("greet", flag.ContinueOnError), } gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted") return gc } type GreetCommand struct { fs *flag.FlagSet name string } func (g *GreetCommand) Name() string { return g.fs.Name() } func (g *GreetCommand) Init(args []string) error { return g.fs.Parse(args) } func (g *GreetCommand) Run() error { fmt.Println("Hello", g.name, "!") return nil } type Runner interface { Init([]string) error Run() error Name() string } func root(args []string) error { if len(args) < 1 { return errors.New("You must pass a sub-command") } cmds := []Runner{ NewGreetCommand(), } subcommand := os.Args[1] for _, cmd := range cmds { if cmd.Name() == subcommand { cmd.Init(os.Args[2:]) return cmd.Run() } } return fmt.Errorf("Unknown subcommand: %s", subcommand) } func main() { if err := root(os.Args[1:]); err != nil { fmt.Println(err) os.Exit(1) } } ينقسم هذا البرنامج إلى عدة أجزاء: الدالة main، والدالة root، وبعض الدوال لتحقيق الأمر الفرعي. تعالج الدالة main الأخطاء التي تُعاد من الأوامر، بينما تلتقط تعليمة if الأخطاء التي قد تُعيدها الدوال وتطبع الخطأ ويُنهى البرنامج وتعاد القيمة 1 إشارةً إلى حدوث خطأ إلى نظام التشغيل. نمرر جميع المعطيات التي استُدعي البرنامج معها داخل الدالةmain إلى الدالة root مع حذف الوسيط الأول الذي يُمثل اسم البرنامج (في الأمثلة السابقة subcommand\.) من خلال استقطاع os.Args. تعرّف الدالة الدالة root الأمر الفرعي Runner[] حيثما تُعرّف جميع الأوامر الفرعية. Runner هي واجهة الأوامر الفرعية التي تسمح لدالة root باسترداد اسم الأمر الفرعي باستخدام ()Name ومقارنته بمحتويات المتغير subcommand. بمجرد تحديد الأمر الفرعي الصحيح بعد التكرار على متغير cmds، نُهيئ الأمر الفرعي مع بقية الوسطاء ونستدعي التابع ()Run الخاص بهذا الأمر. على الرغم من أننا نُعرّف أمرًا فرعيًّا واحدًا، إلا أن هذا الإطار سيسمح لنا بسهولة بإنشاء أوامر أخرى. نُعرّف نسخةً من النوع GreetCommand باستخدام NewGreetCommand، إذ نُنشئ flag.FlagSet* جديد باستخدام flag.NewFlagSet. تأخذ flag.NewFlagSet وسيطين، هما اسم مجموعة الرايات واستراتيجية للإبلاغ عن أخطاء التحليل. يمكن الوصول إلى اسم flag.FlagSet* باستخدام التابعflag.(*FlagSet).Name. نستخدم هذا في الطريقة ()GreetCommand).Name*) بحيث يتطابق اسم الأمر الفرعي مع الاسم الذي قدمناه إلى flag.FlagSet*. يُعرّف NewGreetCommand أيضًا الراية name- بطريقة مشابهة للأمثلة السابقة، ولكنه بدلًا من ذلك يستدعيها مثل تابع للحقل flag.FlagSet* في GreetCommand* (نكتب gc.fs). عندما تستدعي root التابع ()Init الذي يخص GreetCommand*، فإننا نمرر المعطيات المقدمة إلى التابع Parse الخاص بالحقل flag.FlagSet*. سيكون من الأسهل أن ترى الأوامر الفرعية إذا بنيت هذا البرنامج ثم شغلته. اِبنِ البرنامج كما يلي: $ go build subcommand.go الآن، شغّل البرنامج دون وسطاء: $ ./subcommand سيكون الخرج على النحو التالي: You must pass a sub-command شغّل الآن البرنامج مع استخدام الأمر الفرعي greet: $ ./subcommand greet سيكون الخرج على النحو التالي: Hello World ! استخدم الآن الراية name مع greet لتحديد اسم: $ ./subcommand greet -name Sammy سيكون الخرج كما يلي: Hello Sammy ! يوضح هذا المثال بعض المبادئ الكامنة وراء كيفية هيكلة تطبيقات سطر الأوامر الأكبر حجمًا أو الأكثر تعقيدًا في لغة جو. صُمم FlagSets لمنح المطورين مزيدًا من التحكم وكيفية معالجة الرايات من خلال منطق تحليل الراية. الخاتمة تمنح الرايات مستخدمي برنامجك التحكم في كيفية تنفيذ البرامج، أو التحكم بسلوكه، وبالتالي تجعله أكثر مرونةً في التعامل مع سياقات التنفيذ المحتملة. من المهم أن تمنح المستخدمين إعدادات افتراضية مفيدة، ولكن يجب أن تمنحهم الفرصة لتجاوز الإعدادات التي لا تناسب حالتهم. لقد رأينا أن حزمة الراية flag توفر خيارات مرنة لتعزيز إمكانية إضافة خيارات ضبط خاصة للمستخدمين. يمكنك اختيار بعض الرايات البسيطة، أو إنشاء مجموعة من الأوامر الفرعية القابلة للتوسيع. سيساعدك استخدام حزمة الراية في كلتا الحالتين على بناء أدوات مرنة لسطر الأوامر. ترجمة -وبتصرف- للمقال How To Use the Flag Package in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: استخدام الراية ldflags لضبط معلومات الإصدار لتطبيقات لغة جو Go كيفية تعريف واستدعاء الدوال في لغة جو Go البرمجة بلغة go كيفية تعريف واستدعاء الدوال في لغة جو Go
  3. يؤدي بناء الثنائيات أو الملفات التنفيذية Binaries جنبًا إلى جنب مع إنشاء البيانات الوصفية Metadata والمعلومات الأخرى المتعلقة بالإصدار Version عند نشر التطبيقات إلى تحسين عمليات المراقبة Monitoring والتسجيل Logging وتصحيح الأخطاء، وذلك من خلال إضافة معلومات تعريف تساعد في تتبع عمليات البناء التي تُجريها بمرور الوقت. يمكن أن تتضمن معلومات الإصدار العديد من الأشياء التي تتسم بالديناميكية، مثل وقت البناء والجهاز أو المستخدم الذي أجرى عملية بناء الملف التنفيذي ورقم المعرّف ID للإيداع Commit على نظام إدارة الإصدار VCS الذي تستخدمه غيت Git مثلًا. بما أن هذه المعلومات تتغير باستمرار، ستكون كتابة هذه المعلومات ضمن الشيفرة المصدر مباشرةً، وتعديلها في كل مرة نرغب فيها بإجراء تعديل أو بناء جديد أمرًا مملًا، وقد يُعرّض التطبيق لأخطاء. يمكن للملفات المصدرية التنقل وقد تُبدّل المتغيرات أو الثوابت الملفات خلال عملية التطوير، مما يؤدي إلى كسر عملية البناء. إحدى الطرق المستخدمة لحل هذه المشكلة في لغة جو هي استخدام الراية ldflags- مع الأمر go build لإدراج معلومات ديناميكية في الملف الثنائي التنفيذي في وقت البناء دون الحاجة إلى تعديل التعليمات البرمجية المصدرية. تُشير Id ضمن الراية السابقة إلى الرابط linker، الذي يُمثّل البرنامج الذي يربط الأجزاء المختلفة من الشيفرة المصدرية المُصرّفة مع الملف التنفيذي النهائي. إذًا، تعني ldflags رايات الرابط linker flags؛ لأنها تمرر إشارة إلى الأداة cmd/link الخاصة بلغة جو، والتي تسمح لك بتغيير قيم الحزم المستوردة في وقت البناء من سطر الأوامر. سنستخدم في هذه المقالة الراية ldflags- لتغيير قيمة المتغيرات في وقت البناء وإدخال المعلومات الديناميكية ضمن ملف ثنائي تنفيذي، من خلال تطبيق يطبع معلومات الإصدار على الشاشة. المتطلبات أن يكون لديك مساحة عمل خاصة في لغة جو. لقد تحدّثنا عن ذلك في بداية السلسلة في المقال تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu، وتثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS، وتثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز. بناء تطبيق تجريبي يجب أن يكون لدينا تطبيق حتى نستطيع تجريب عملية إدراج المعلومات إليه دينامكيًا من خلال الراية ldflags-. سنبني في هذه الخطوة تطبيقًا بسيطًا للتجريب عليه، بحيث يطبع المعلومات الخاصة بالإصدار. بدايةً أنشئ مجلدًا باسم app -يُمثّل اسم التطبيق- داخل المجلد src: $ mkdir app انتقل إلى هذا المجلد: $ cd app أنشئ باستخدام محرر النصوص الذي تُفضّله وليكن نانو nano الملف main.go: $ nano main.go ضع الشيفرة التالية بداخل هذا الملف، والتي تؤدي إلى طباعة معلومات الإصدار الحالي من التطبيق: package main import ( "fmt" ) var Version = "development" func main() { fmt.Println("Version:\t", Version) } صرّحنا داخل الدالة main عن متغير يُدعى Version، ثم طبعنا السلسلة النصية :Version متبوعةً بإزاحة جدول واحدة tab كما يلي: t\، ثم قيمة المتغير Version. هنا أعطينا متغير الإصدار القيمة development، والتي ستكون إشارةً إلى الإصدار الافتراضي من التطبيق. سنُعدّل لاحقًا قيمة هذا المتغير، بحيث تُشير إلى الإصدار الرسمي من التطبيق، ووفقًا للتنسيق المُتبع في تسمية الإصدارات. احفظ واغلق الملف، ثم ابنِ الملف وشغّله للتأكد من أنه يعمل: $ go build $ ./app ستحصل على الخرج التالي: Version: development لديك الآن تطبيق يطبع معلومات الإصدار الافتراضي، ولكن ليس لديك حتى الآن طريقةً لتمرير معلومات الإصدار الحالي في وقت البناء. ستستخدم في الخطوة التالية الراية ldflags- لحل هذه المشكلة. استخدام ldflags مع go build تحدّثنا سابقًا أن رايات الربط تُستخدم لتمرير الرايات إلى أداة الربط الأساسية underlying الخاصة بلغة جو. يحدث ذلك وفقًا للصيغة التالية: $ go build -ldflags="-flag" هنا مرّرنا flag إلى الأداة الأساسية go tool link التي تعمل بمثابة جزء من الأمر go build. هنا وضعنا علامتي اقتباس حول القيمة التي نمررها إلى ldflags، وذلك لكي نمنع حدوث التباس لدى سطر الأوامر (لكي لا يفسرها بطريقة خاطئة أو يعدها عدة محارف كل منها لغرض مختلف). يمكنك تمرير العديد من رايات الروابط، وفي هذه المقالة سنحتاج إلى استخدام الراية X- لضبط معلومات متغير الإصدار في وقت الربط link time، وسيتبعها مسار المتغير (هنا اسم الحزمة متبوعة باسم المتغير) مع قيمته الجديدة. $ go build -ldflags="-X 'package_path.variable_name=new_value'" لاحظ أننا وضعنا الخيار X- ضمن علامتي اقتباس وبجانبها وضعنا اسم الحزمة متبوعةً بنقطة "." متبوعةً باسم المتغير والقيمة الجديدة وأحطناهم بعلامات اقتباس مفردة لكي يُفسرها سطر الأوامر على أنها كتلة واحدة. إذًا، سنستخدم الصيغة السابقة لاستبدال قيمة متغير الإصدار Version في تطبيقنا: $ go build -ldflags="-X 'main.Version=v1.0.0'" تمثّل main هنا مسار الحزمة للمتغير Version، لأنه يتواجد داخل الملف main.go. هنا Version هو المتغير المطلوب تعديله، والقيمة v1.0.0 هي القيمة الجديدة التي نريد ضبطه عليها. عندما نستخدم الراية ldflags، يجب أن تكون القيمة التي تريد تغييرها موجودة وأن يكون المتغير موجودًا ضمن مستوى الحزمة ومن نوع string. لا يُسمح بأن يكون المتغير ثابتًا const، أو أن تُضبط قيمته من خلال استدعاء دالة. يتوافق كل شيء هنا مع المتطلبات، لذا ستعمل الأمور كما هو متوقع؛ فالمتغير موجود ضمن الملف main.go والمتغير والقيمة v1.0.0 التي نريد ضبط المتغير عليها كلاهما من النوع string. شغّل التطبيق بعد بنائه: $./app ستحصل على الخرج التالي: Version: v1.0.0 إذًا، استطعنا من خلال الراية ldflags- تعديل قيمة متغير الإصدار من development إلى v1.0.0 في وقت البناء. يمكنك تضمين تفاصيل الإصدار ومعلومات الترخيص وغيرهم من المعلومات باستخدام ldflags- جنبًا إلى جنب مع الملف التنفيذي النهائي الذي ترغب بنشره وذلك فقط من خلال سطر الأوامر. في هذا المثال: كان المتغير موجود في مسار واضح، لكن في أمثلة أخرى قد يكون العثور على مسار المتغير أمرًا معقدًا. سنناقش في الخطوة التالية هذا الموضوع، وسنرى ماهي أفضل الطرق لتحديد مسارات المتغيرات الموجودة في حزم فرعية في الحزم ذات الهيكلية الأكثر تعقيدًا. تحديد مسار الحزمة للمتغيرات كنا قد وضعنا في المثال السابق متغير الإصدار Version ضمن المستوى الأعلى من الحزمة في الملف main.go، لذا كان أمر الوصول إليه بسيطًا. هذه حالة مثالية، لكن في الواقع هذا لايحدث دائمًا، فأحيانًا تكون المتغيرات ضمن حزمة فرعية أخرى. عمومًا، لا يُحبذ وضع هكذا متغيرات ضمن main، لأنه حزمة غير قابلة للاستيراد، ويُفضّل وضع هكذا متغيرات ضمن حزمة أخرى. لذا سنعدل بعض الأمور في تطبيقنا، إذ سنُنشئ الحزمة app/build ونضع فيها معلومات حول وقت بناء الملف التنفيذي واسم المستخدم الذي بناه. انشئ مجلدًا جديدًا باسم الحزمة الجديدة: $ mkdir -p build انشئ ملفًا جديدًا باسم build.go من أجل وضع المتغير/المتغيرات ضمنه: $ nano build/build.go ضع بداخله المتغيرات التالية بعد فتحه باستخدام محرر النصوص الذي تريده: package build var Time string # سيُخزّن وقت بناء التطبيق var User string # سيُخزّن اسم المستخدم الذي بناه لا يمكن لهذين المتغيرين أن يتواجدا بدون قيم، لذا لا داعٍ لوضع قيم افتراضية لهما كما فعلنا مع متغير الإصدار. احفظ وأغلق الملف. افتح ملف main.go لوضع المتغيرات داخله: $ nano main.go ضع فيه المحتويات التالية: package main import ( "app/build" "fmt" ) var Version = "development" func main() { fmt.Println("Version:\t", Version) fmt.Println("build.Time:\t", build.Time) fmt.Println("build.User:\t", build.User) } استوردنا الحزمة app/build، ثم طبعنا قيمة build.Time و build.User بنفس الطريقة التي طبعنا فيها Version سابقًا. احفظ وأغلِق الملف. إذا أردت الآن الوصول إلى هذه المتغيرات عند استخدام الراية ldflags-، يمكنك استخدام اسم الحزمة app/build يتبعها Time. أو User. كوننا نعرف مسار الحزمة. سنستخدم الأمر nm بدلًا من ذلك من أجل محاكاة موقف أكثر تعقيدًا، والذي يكون فيه مسار الحزمة غير واضح. يُنتج الأمر go tool nm الرموز المتضمنة في ملف تنفيذي أو ملف كائن أو أرشيف، إذ يشير الرمز إلى كائن موجود في الشيفرة (متغير أو دالة مُعرّفة أو مستوردة). يمكنك العثور بسرعة على معلومات المسار من خلال إنشاء جدول رموز باستخدام nm واستخدام grep للبحث عن متغير. ملاحظة: لن يساعدك الأمر nm في العثور على مسار المتغير إذا كان اسم الحزمة يحتوي على أي محارف ليست ASCII، أو " أو ٪ (هذه قيود خاصة بالأداة). ابنِ التطبيق أولًا لاستخدام هذا الأمر: $ go build وجّه الأداة nm إلى التطبيق بعد بنائه وابحث في الخرج: $ go tool nm ./app | grep app عند تشغيل الأمر nm ستحصل على العديد من البيانات، لذا وضعنا | لتوجيه الخرج إلى الأمر grep الذي يبحث بعد ذلك عن المسارات التي تحتوي على الاسم app في المستوى الأعلى منها. ستحصل على الخرج التالي: 55d2c0 D app/build.Time 55d2d0 D app/build.User 4069a0 T runtime.appendIntStr 462580 T strconv.appendEscapedRune . . . يظهر في أول سطرين مسارات المتغيرات التي تبحث عنها: app/build.Time و app/build.User. ابنِ التطبيق الآن بعد أن تعرفت على المسارات، وعدّل متغير الإصدار إضافةً إلى المتغيرات الجديدة التي أضفناها والتي تُمثّل وقت بناء التطبيق واسم المستخدم (تذكر أنك تُعدّل هذه المتغيرات في وقت البناء). لأجل ذلك ستحتاج إلى تمرير عدة رايات X- إلى الراية ldflags-: $ go build -v -ldflags="-X 'main.Version=v1.0.0' -X 'app/build.User=$(id -u -n)' -X 'app/build.Time=$(date)'" هنا مررنا الأمر id -u -n لعرض المستخدم الحالي، والأمر date لعرض التاريخ الحالي. شغّل التطبيق بعد بنائه: $ ./app ستحصل على الخرج التالي في حال كنت تعمل على نظام يستند إلى يونيكس Unix: Version: v1.0.0 build.Time: Fri Oct 4 19:49:19 UTC 2019 build.User: sammy لديك الآن ملف تنفيذي يتضمن معلومات الإصدار والبناء، والتي يمكن أن توفر مساعدة حيوية في الإنتاج عند حل المشكلات. الخاتمة وضّحت هذه المقالة مدى قوة استخدام ldflags لإدخال معلومات في وقت البناء إذا طُبقت بطريقة سليمة. يمكنك بهذه الطريقة التحكم في رايات الميزة feature flags (هي تقنية برمجية تُمكّن الفريق البرمجي من إجراء تغييرات بدون استخدام المزيد من التعليمات البرمجية) ومعلومات البيئة ومعلومات الإصدار والأمور الأخرى دون إدخال تغييرات على الشيفرة المصدر. يمكنك الاستفادة من الخصائص التي تمنحك إياها لغة جو لعمليات النشر من خلال استخدام ldflags في عمليات البناء. ترجمة -وبتصرف- للمقال Using ldflags to Set Version Information for Go Applications لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: بناء تطبيقات لغة Go على أنظمة التشغيل والمعماريات المختلفة استخدام وسوم البناء لتخصيص الملفات التنفيذية Binaries في لغة جو Go بناء البرامج المكتوبة بلغة جو Go وتثبيتها
  4. عند تطوير البرمجيات من المهم الأخذ بالحسبان نوع نظام التشغيل الذي تبني التطبيق عليه والمعمارية التي ستُصرّف تطبيقك من أجلها إلى ملف ثنائي تنفيذي Binary، وتكون عملية تشغيل التطبيق على نظام تشغيل مختلف أو معمارية مختلفة غالبًا عمليةً بطيئة أو مستحيلة أحيانًا، لذا من الممارسات العمليّة الشائعة بناء ملف تنفيذي يعمل على العديد من المنصات المختلفة، وذلك لزيادة شعبية وعدد مستخدمي تطبيقك، إلا أن ذلك غالبًا ما يكون صعبًا عندما تختلف المنصة التي تطوّر تطبيقك عليها عن المنصة التي تريد نشره عليها؛ إذ كان يتطلب مثلًا تطوير برنامج على ويندوز ونشره على لينوكس Linux أو ماك أو إس MacOS سابقًا إعدادات بناء مُحددة من أجل كل بيئة تُريد تصريف البرنامج من أجلها، ويتطلب الأمر أيضًا الحفاظ على مزامنة الأدوات، إضافةً إلى الاعتبارات الأخرى التي قد تضيف تكلفةً وتجعل الاختبار التعاوني Collaborative Testing والنشر أكثر صعوبة. تحل لغة جو هذه المشكلة عن طريق بناء دعم لمنصّات متعددة مباشرةً من خلال الأداة go build وبقية أدوات اللغة. يمكنك باستخدام متغيرات البيئة ووسوم البناء التحكم في نظام التشغيل والمعمارية التي بُنى الثنائي النهائي من أجلها، إضافةً إلى وضع مُخطط لسير العمل يمكن من خلاله التبديل إلى التعليمات البرمجية المُضمّنة والمتوافقة مع المنصة المطلوب تشغيل التطبيق عليها دون تغيير الشيفرة البرمجية الأساسية. ستُنشئ في هذا المقال تطبيقًا يربط السلاسل النصية مع بعضها في مسار ملف، إضافةً إلى كتابة شيفرات برمجية يمكن تضمينها اختياريًّا، يعتمد كلٌ منها على منصة مُحددة. ستُنشئ ملفات تنفيذية لأنظمة تشغيل ومعماريات مختلفة على نظامك الخاص، وسيُبيّن لك ذلك كم أن لغة جو قوية في هذا الجانب. ملاحظة: في تكنولوجيا المعلومات، المنصة هي أي عتاد Hardware أو برمجية Software تُستخدم لاستضافة تطبيق أو خدمة. على سبيل المثال قد تتكون من عتاديات ونظام تشغيل وبرامج أخرى تستخدم مجموعة التعليمات الخاصة بالمعالج. المتطلبات تفترض هذه المقالة أنك على دراية بوسوم البناء في لغة جو، وإذا لم يكن لديك معرفةً بها، راجع مقالة استخدام وسوم البناء لتخصيص الملفات التنفيذية Binaries. أن يكون لديك مساحة عمل خاصة في لغة جو، وكنا قد تحدّثنا عن ذلك في بداية السلسلة في المقال تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu، وتثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS، وتثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز. المنصات التي يمكن أن تبني لها تطبيقك باستخدام لغة جو قبل أن نعرض كيف يمكننا التحكم بعملية البناء من أجل بناء ملفات تنفيذية تتوافق مع منصات مختلفة، سنستعرض أولًا أنواع المنصات التي يمكن لجو البناء من أجلها، وكيف تشير جو إلى هذه المنصات باستخدام متغيرات البيئة GOOS و GOARCH. يمكن عرض قائمة بأسماء المنصات التي يمكن لجو أن تبني تطبيقًا من أجلها، وتختلف هذه القائمة من إصدار لآخر، لذا قد تكون القائمة التي سنعرضها عليك مختلفةً عن القائمة التي تظهر عندك وذلك تبعًا لإصدار جو الذي تعمل عليه (الإصدار الحالي 1.13). نفّذ الأمر التالي لعرض القائمة: $ go tool dist list ستحصل على الخرج التالي: aix/ppc64 freebsd/amd64 linux/mipsle openbsd/386 android/386 freebsd/arm linux/ppc64 openbsd/amd64 android/amd64 illumos/amd64 linux/ppc64le openbsd/arm android/arm js/wasm linux/s390x openbsd/arm64 android/arm64 linux/386 nacl/386 plan9/386 darwin/386 linux/amd64 nacl/amd64p32 plan9/amd64 darwin/amd64 linux/arm nacl/arm plan9/arm darwin/arm linux/arm64 netbsd/386 solaris/amd64 darwin/arm64 linux/mips netbsd/amd64 windows/386 dragonfly/amd64 linux/mips64 netbsd/arm windows/amd64 freebsd/386 linux/mips64le netbsd/arm64 windows/arm نلاحظ أن الخرج هو مجموعة من أزواج المفتاح والقيمة key-value مفصولة بالرمز "/"؛ إذ يمثّل المفتاح key (على اليسار) نظام التشغيل. تُعد هذه الأنظمة قيمًا محتملة لمتغير البيئة GOOS (يُنطق "goose")، اختصارًا إلى Go Operating System؛ بينما تشير القيمة value (على اليمين) إلى المعمارية، وتمثّل القيم المُحتملة لمتغير البيئة GOARCH (تُنطق "gore-ch") وهي اختصار Go Architecture. دعنا نأخذ مثال linux/386 لفهم ما تعنيه وكيف يجري الأمر: تُمثّل linux المفتاح وهي قيمة المتغير GOOS، بينما تمثّل 386 المعالج Intel 80386 والتي ستكون هي القيمة، وتُمثّل قيمة المتغير GOARCH. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن هناك العديد من المنصات التي يمكن أن تتعامل معها من خلال الأداة go build، لكن غالبًا سيكون تعاملك مع منصة لينكس أو ويندوز أو داروين darwin في قيم المتغير GOOS. إذًا، هذا يُغطي المنصات الثلاثة الأكبر: ويندوز ولينكس وماك، إذ يعتمد الأخير على نظام التشغيل داروين. يمكن لجو عمومًا تغطية المنصات الأقل شهرة مثل nacl. عند تشغيل أمر مثل go build، يستخدم جو مُتغيرات البيئة GOOS و GOARCH المرتبطين بالمنصة الحالية، لتحديد كيفية بناء الثنائي. لمعرفة تركيبة المفتاح-قيمة للمنصة التي تعمل عليها، يمكنك استخدام الأمر go env وتمرير GOOS و GOARCH مثل وسيطين: $ go env GOOS GOARCH يعمل الجهاز الذي نستخدمه بنظام ماك ومعمارية AMD64 لذا سيكون الخرج: darwin amd64 أي أن منصتنا لديها القيم التالية لمتغيرات البيئة GOOS=darwin و GOARCH=amd64. أنت الآن تعرف ما هي GOOS و GOARCH، إضافةً إلى قيمهما المحتملة. سنكتب الآن برنامجًا لاستخدامه مثالًا على كيفية استخدام متغيرات البيئة هذه ووسوم البناء، بهدف بناء ملفات تنفيذية لمنصات أخرى. بناء تطبيق يعتمد على المنصة سنبدأ أولًا ببناء برنامج بسيط، ومن الأمثلة الجيدة لهذا الغرض الدالة Join من الحزمة path/filepath من مكتبة جو القياسية، إذ تأخذ هذه الدالة عدة سلاسل وتُرجع سلسلةً مكونةً من تلك السلاسل بعد ربطها اعتمادًا على فاصل مسار الملف filepath. يُعد هذا المثال التوضيحي مناسبًا لأن تشغيل البرنامج يعتمد على نظام التشغيل الذي يعمل عليه، ففي نظام التشغيل ويندوز، يكون فاصل المسار \، بينما تستخدم الأنظمة المستندة إلى يونيكس Unix الفاصل /. سنبدأ ببناء تطبيق يستخدم ()filepath.Join، وسنكتب لاحقًا تنفيذًا خاصًا لهذه الدالة يُخصص الشيفرة للثنائيات التي تتبع لمنصة محددة. أنشئ مجلدًا داخل المجلد src باسم تطبيقك: $ mkdir app انتقل إلى المجلد: $ cd app أنشئ ملفًا باسم main.go من خلال محرر النصوص نانو nano أو أي محرر آخر: $ nano main.go ضع في الملف الشيفرة التالية: package main import ( "fmt" "path/filepath" ) func main() { s := filepath.Join("a", "b", "c") fmt.Println(s) } تستخدم الدالة الرئيسية ()main هنا الدالة ()filepath.Join لربط ثلاث سلاسل مع فاصل المسار الصحيح المعتمد على المنصة. احفظ الملف واخرج منه، ثم نفّذه من خلال الأمر التالي: $ go run main.go عند تشغيل هذا البرنامج، ستتلقى مخرجات مختلفة بناءً على المنصة التي تستخدمها، ففي نظام التشغيل ويندوز، سترى السلاسل مفصولة بالفاصل \: a\b\c أما في أنظمة يونِكس مثل ماك ولِنُكس: a/b/c يوضح هذا أن اختلاف بروتوكولات نظام الملفات المستخدمة في أنظمة التشغيل هذه، يقتضي على البرنامج بناء شيفرات مختلفة للمنصات المختلفة. نحن نعلم أن الاختلاف هنا سيكون بفاصل الملفات كما تحدثنا، وبما أننا نستخدم الدالة ()filepath.Join فلا خوف من ذلك، لأنها ستأخذ بالحسبان اختلاف نظام التشغيل الذي تُستخدم ضمنه. تفحص سلسلة أدوات جو تلقائيًا GOOS و GOARCH في جهازك وتستخدم هذه المعلومات لاستخدام الشيفرة المناسبة مع وسوم البناء الصحيحة وفاصل الملفات المناسب. سنرى الآن من أين تحصل الدالة ()filepath.Join على الفاصل المناسب. شغّل الأمر التالي لفحص المقتطف ذي الصلة من مكتبة جو القياسية: $ less /usr/local/go/src/os/path_unix.go سيُظهر هذا الأمر محتويات الملف "path_unix.go". ألقِ نظرةً على السطر الأول منه، والذي يُمثّل وسوم البناء. . . . // +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris package os const ( PathSeparator = '/' // OS-specific path separator PathListSeparator = ':' // OS-specific path list separator ) . . . تعرّف الشيفرة الفاصل PathSeparator المستخدم مع الأنواع المختلفة من الأنظمة التي تستند على يُنِكس والتي يدعمها جو. لاحظ في السطر الأول وسوم البناء التي تمثل كل واحدة منها قيمةً محتملةً للمتغير GOOS وهي جميعها تمثل أنظمة تستند إلى ينكس. يأخذ GOOS أحد هذه القيم ويُنتج الفاصل المناسب تبعًا لنوع النظام. اضغط q للعودة لسطر الأوامر. افتح الآن ملف path_windows الذي يُعبر عن سلوك الدالة ()filepath.Join على نظام ويندوز: . . . package os const ( PathSeparator = '\\' // OS-specific path separator PathListSeparator = ';' // OS-specific path list separator ) . . . على الرغم من أن قيمة PathSeparator هنا هي \\، إلا أن الشيفرة ستعرض الشرطة المائلة الخلفية المفردة \ اللازمة لمسارات ملفات ويندوز، إذ تُستخدم الشرطة المائلة الأولى بمثابة مفتاح هروب Escape character. لاحظ أنه في الملف الخاص بينكس كان لدينا وسوم بناء، أما في ملف ويندوز فلا يوجد وسوم بناء، وذلك لأن GOOS و GOARCH يمكن تمريرهما أيضًا إلى go build عن طريق إضافة شرطة سفلية "_" وقيمة متغير البيئة مثل لاحقة suffix لاسم الملف (سنتحدث عن ذلك أكثر بعد قليل). يجعل الجزء windows_ من path_windows.go الملف يعمل كما لو كان يحتوي على وسم البناء build windows+ // في أعلى الملف، لذلك عند تشغيل البرنامج على ويندوز، سيستخدم الثوابت PathSeparator و PathListSeparator من الشيفرة الموجودة في الملف "path_windows.go". اضغط q للعودة لسطر الأوامر. أنشأت في هذه الخطوة برنامجًا يوضّح كيف يمكن لجو أن يجري التبديل تلقائيًا بين الشيفرات من خلال متغيرات البيئة GOOS و GOARCH ووسوم البناء. يمكنك الآن تحديث برنامجك وكتابة تنفيذك الخاص للدالة ()filepath.Join، والاعتماد على وسوم البناء لتحديد الفاصل PathSeparator المناسب لمنصات ويندوز ويونيكس يدويًا. تنفيذ دالة خاصة بالمنصة بعد أن تعرّفت على كيفية تحقيق مكتبة جو القياسية للتعليمات البرمجية الخاصة بالمنصة، يمكنك استخدام وسوم البناء لأجل ذلك في تطبيقك. ستكتب الآن تعريفًا خاصًا للدالة ()filepath.Join. افتح ملف main.go الخاص بتطبيقك: $ nano main.go استبدل محتويات main.go وضع فيه الشيفرة التالية التي تتضمن دالة خاصة اسميناها Join: package main import ( "fmt" "strings" ) func Join(parts ...string) string { return strings.Join(parts, PathSeparator) } func main() { s := Join("a", "b", "c") fmt.Println(s) } تأخذ الدالة Join عدة سلاسل نصية من خلال المعامل parts وتربطهما معًا باستخدام الفاصل PathSeparator، وذلك من خلال الدالة ()strings.Join من حزمة strings. لم نُعرّف PathSeparator بعد، لذا سنُعرّفه الآن في ملف آخر. احفظ main.go واخرج منه، وافتح المحرر المفضل لديك، وأنشئ ملفًا جديدًا باسم path.go: nano path.go صرّح عن الثابت PathSeparator وأسند له فاصل المسارات الخاص بملفات يونيكس "/": package main const PathSeparator = "/" صرّف التطبيق وشغّله: $ go build $ ./app ستحصل على الخرج التالي: a/b/c هذا جيد بالنسبة لأنظمة ينكس، لكنه ليس ما نريده تمامًا، فهو يُعطي دومًا a/b/c بغض النظر عن المنصة، وهذا لا يتناسب مع ويندوز مثلًا. إذًا، نحن بحاجة إلى نسخة خاصة من PathSeparator لنظام ويندوز، وإخبار go build أي من هذه النسخ يجب استخدامها وفقًا للمنصة المطلوبة. هنا يأتي دور وسوم البناء. استخدام وسوم البناء مع متغيرات البيئة لكي نعالج حالة كون النظام هو ويندوز، سنُنشئ الآن ملفًا بديلًا للملف path.go وسنضيف وسوم بناء Build Tags مهمتها المطابقة مع المتغيرات GOOS و GOARCH، وذلك لضمان أن الشيفرة المستخدمة هي الشيفرة التي تعمل على المنصة المحددة. أضف بدايةً وسم بناء إلى الملف "path.go" لإخباره أن يبني لكل شيء باستثناء ويندوز، افتح الملف: $ nano path.go أضِف وسم البناء التالي إلى الملف: // +build !windows package main const PathSeparator = "/" تُقدّم وسوم البناء في لغة جو إمكانية "العكس inverting" مما يعني أنه يمكنك توجيه جو لبناء هذا الملف من أجل أي منصة باستثناء ويندوز. لعكس وسم بناء، ضع "!" قبل الوسم كما فعلنا أعلاه. احفظ واخرج من الملف. والآن إذا حاولت تشغيل هذا البرنامج على ويندوز، ستتلقى الخطأ التالي: ./main.go:9:29: undefined: PathSeparator في هذه الحالة لن تكون جو قادرةً على تضمين path.go لتعريف فاصل المسار PathSeparator. الآن بعد أن تأكدت من أن path.go لن يعمل عندما يكون GOOS هو ويندوز. أنشئ ملفًا جديدًا windows.go: $ nano windows.go أضِف ضمن هذا الملف PathSeparator ووسم بناء أيضًا لإخبار الأمر go build أن هذا الملف هو التحقيق المقابل للويندوز: // +build windows package main const PathSeparator = "\\" احفظ الملف واخرج من محرر النصوص. يمكن للتطبيق الآن تصريف نسخة لنظام ويندوز ونسخة أخرى لباقي الأنظمة. ستُبنى الآن ملفات تنفيذية بطريقة صحيحة وفقًا للمنصة المطلوبة، إلا أنه هناك المزيد من التغييرات التي يجب عليك إجراؤها من أجل التصريف على المنصة التي لا يمكنك الوصول إليها. سنُعدّل في الخطوة التالية متغيرات البيئة المحلية GOOS و GOARCH. استخدام متغيرات البيئة المحلية GOOS و GOARCH استخدمنا سابقًا الأمر go env GOOS GOARCH لمعرفة النظام والمعمارية التي تعمل عليها. عند استخدام الأمر go env، سيبحث عن المتغيرين GOOS و GOARCH، فإذا وجدهما سيستخدم قيمهما وإلا سيفترض أن قيمهما هي معلومات المنصة الحالية (نظام ومعمارية المنصة). نستنتج مما سبق أنه بإمكاننا تحديد نظام ومعمارية لمتغيرات البيئة غير نظام ومعمارية المنصة الحالية. يعمل الأمر go build بطريقة مشابهة للأمر السابق، إذ يمكنك تحديد قيم لمتغيرات البيئة GOOS و GOARCH مختلفة عن المنصة الحالية. إذا كنت لا تستخدم نظام ويندوز، ابنِ نسخةً ثنائيةً من تطبيقك لنظام ويندوز عن طريق تعيين قيمة متغير البيئة GOOS على windows عند تشغيل الأمر go build: $ GOOS=windows go build اسرد الآن محتويات مجلدك الحالي: $ ls ستجد في الخرج ملفًا باسم app.exe، إذ يكون امتداده exe والذي يشير إلى ملف ثنائي تنفيذي في نظام ويندوز. app app.exe main.go path.go windows.go يمكنك باستخدام الأمر file الحصول على مزيد من المعلومات حول هذا الملف، للتأكد من بنائه: $ file app.exe ستحصل على: app.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows يمكنك أيضًا إعداد واحد أو اثنين من متغيرات البيئة أثناء وقت البناء. نفّذ الأمر التالي: $ GOOS=linux GOARCH=ppc64 go build بذلك يكون قد استُبدل ملفك التنفيذي app بملف لمعمارية مختلفة. شغّل الأمر file على تطبيقك الثنائي: $ file app ستحصل على الخرج التالي: app: ELF 64-bit MSB executable, 64-bit PowerPC or cisco 7500, version 1 (SYSV), statically linked, not stripped من خلال متغيرات البيئة GOOS و GOARCH يمكنك الآن إنشاء تطبيق قابل للتنفيذ على منصات مختلفة، دون الحاجة إلى إعدادات ضبط مُعقدة. سنستخدم في الخطوة التالية بعض "الاصطلاحات" لأسماء الملفات لكي نجعل تنسيقها دقيقًا، إضافةً إلى البناء تلقائيًّا من دون الحاجة إلى وسوم البناء. استخدام لواحق اسم الملف مثل دليل إلى المنصة المطلوبة كما لاحظت سابقًا، تعتمد جو على استخدام وسوم البناء كثيرًا لفصل الإصدارات أو النسخ المختلفة من التطبيق إلى ملفات مختلفة بغية تبسيط التعليمات البرمجية، فعندما فتحت ملف "os/path_unix.go" كان هناك وسم بناء يعرض جميع التركيبات المحتملة التي تُعبر عن منصات شبيهة أو تستند إلى ينكس، إلا أن ملف "os/path_windows.go" لا يحتوي على وسوم بناء، لأن لاحقة اسم الملف كانت كافية لإخبار جو بالمنصة المطلوبة. دعونا نلقي نظرةً على تركيب أو قاعدة هذه الميزة، فعند تسمية ملف امتداده "go."، يمكنك إضافة GOOS و GOARCH مثل لواحق إلى اسم الملف بالترتيب، مع فصل القيم عن طريق الشرطات السفلية "_". إذا كان لديك ملف جو يُسمى filename.go، يمكنك تحديد نظام التشغيل والمعمارية عن طريق تغيير اسم الملف إلى filename_GOOSGO_ARCH.go. على سبيل المثال، إذا كنت ترغب في تصريفه لنظام التشغيل ويندوز باستخدام معمارية ‎64-bit ARM، فيمكنك كتابة اسم الملف filename_windows_arm64.go، إذ يساعد اصطلاح التسمية هذا في الحفاظ على الشيفرة منظمةً بدقة. حدّث البرنامج الآن بحيث نستخدم لاحقات اسم الملف بدلًا من وسوم البناء، وأعد تسمية ملف "path.go" و "windows.go" لاستخدام الاصطلاح المستخدم في حزمةos: $ mv path.go path_unix.go $ mv windows.go path_windows.go بعد تعديل اسم الملف أصبح بإمكانك حذف وسم البناء من ملف "path_windows.go": $ nano path_windows.go بعد حذف build windows+ // سيكون ملفك كما يلي: package main const PathSeparator = "\\" احفظ الملف واخرج منه. بما أن unix هي قيمة غير صالحة للمتغير GOOS، فإن اللاحقة unix.go_ ليس لها أي معنى لمُصرّف جو، وبالرغم من ذلك، فإنه ينقل الغاية المرجوة من الملف. لا يزال مثلًا ملف os/path_unix.go بحاجة إلى استخدام وسوم البناء، لذا يجب الاحتفاظ بهذا الملف دون تغيير. نستنتج أنه من خلال الاصطلاحات استطعنا التخلص من وسوم البناء غير الضرورية التي أضفناها إلى شيفرات التطبيق، كما جعلنا نظام الملفات أكثر تنظيمًا ووضوحًا. الخاتمة لا تحتاج لغة جو إلى أدوات إضافية لدعم فكرة المنصات المتعددة، وهذه ميزة قوية في جو. تعلمنا في هذه المقالة استخدام هذه الإمكانية عن طريق إضافة وسوم البناء واللواحق لاسم الملف، وذلك لتحديد الشيفرات البرمجية التي يجب تنفيذها وفقًا للمنصة المطلوب العمل عليه. أنشأنا تطبيقًا متعدد المنصات وتعلمنا كيفية التعامل مع متغيرات البيئة GOOS و GOARCH لبناء ملفات تنفيذية لمنصات أخرى تختلف عن المنصة الحالية. تعدد المنصات أمر مهم جدًا، فهو يُضيف ميزةً مهمة لتطبيقك، إذ تُمكنه من التصريف وفقًا للمنصة المطلوبة من خلال متغيرات البيئة هذه. ترجمة -وبتصرف- للمقال Building Go Applications for Different Operating Systems and Architectures لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: كيفية استخدام الواجهات Interfaces في لغة جو Go بناء البرامج المكتوبة بلغة جو Go وتثبيتها ما هو نظام التشغيل لينكس؟
  5. أهم الصفات التي يجب أن تتمتع بها البرامج أو التعليمات البرمجية التي نكتبها، هي المرونة وإمكانية إعادة الاستخدام، إضافةً إلى الصفة التركيبية modular، إذ تُعد هذه الصفات الثلاثة أمرًا ضروريًّا لتطوير برامج متعددة الاستخدامات، كما أنها تُسهّل عمليات التعديل والصيانة على البرامج، فمثلًا إذا احتجنا لتعديل بسيط على البرنامج، سيكون بالإمكان إجراء هذا التعديل في مكانٍ واحد بدلًا من إجراء نفس التعديل في أماكن متعددة من البرنامج. تختلف كيفية تحقيق تلك الصفات من لغة إلى أخرى، فعلى سبيل المثال، يٌستخدم مفهوم الوراثة في لغات مثل جافا Java و ++C و #C لتحقيق ذلك. يمكن للمطورين أيضًا تحقيق أهداف التصميم هذه من خلال التركيب Composition، وهي طريقة لدمج الكائنات أو أنواع البيانات في أنواع أكثر تعقيدًا، وهو النهج المُستخدم في لغة جو لتحقيق الصفات السابقة. توفِّر الواجهات في لغة جو توابعًا لتنظيم التراكيب المعقدة، وسيتيح لك تعلم كيفية استخدامها إنشاء شيفرة مشتركة وقابلة لإعادة الاستخدام. ستتعلم في هذا المقال كيفية تركيب أنواع مخصصة لها سلوكيات مشتركة، مما سيتيح لنا إعادة استخدام الشيفرة التي نكتبها، وسنتعلم أيضًا كيفية تحقيق الواجهات للأنواع المخصصة التي تتوافق مع الواجهات المُعرّفة في حزم أخرى. تعريف سلوك Behavior تُعد الواجهات من العناصر الأساسية للتركيب، فهي تعرّف سلوك نوعٍ ما، وتُعد الواجهة fmt.Stringer من أكثر الواجهات شيوعًا في مكتبة جو القياسية: type Stringer interface { String() string } نُعرّف في السطر الأول من الشيفرة السابقة نوعًا جديدًا يُدعى Stringer، ونحدد أنه واجهة. بعد ذلك، نكتب محتويات هذه البنية بين قوسين {}، إذ ستُعرِّف هذه المحتويات سلوك الواجهة، أي ما الذي تفعله الواجهة؛ فبالنسبة للواجهة السابقة Stringer، من الواضح أن السلوك الوحيد فيها هو تابع ()String لا يأخذ أي وسطاء ويعيد سلسلةً نصية. سنرى الآن بعض المقتطفات البرمجية التي تمتلك سلوك الواجهة fmt.Stringer: package main import "fmt" type Article struct { Title string Author string } func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } func main() { a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } fmt.Println(a.String()) } هنا نُنشئ نوعًا جديدًا اسمه Article، ويمتلك حقلين هما Title و Author، وكلاهما من نوع سلاسل نصية. ... type Article struct { Title string Author string } … نُعرّف بعد ذلك تابعًا يسمى String على النوع Article، بحيث يعيد هذا التابع سلسلةً تمثل هذا النوع، أي محتوياته عمليًّا: ... func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } ... نُعرّف بعد ذلك في الدالة main متغيرًا من النوع Article ونسميه a، ونسند السلسلة "Understanding Interfaces in Go" إلى الحقل Title والسلسلة "Sammy Shark" للحقل Author: ... a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } ... نطبع بعد ذلك نتيجة التابع String من خلال استدعاء الدالة fmt.Println وتمرير نتيجة استدعاء التابع ()a.String إليها: ... fmt.Println(a.String()) ستحصل عند تشغيل البرنامج على: The "Understanding Interfaces in Go" article was written by Sammy Shark. لم نستخدم واجهةً حتى الآن، لكننا أنشأنا نوعًا يمتلك سلوكًا يطابق سلوك الواجهة fmt.Stringer. دعنا نرى كيف يمكننا استخدام هذا السلوك لجعل الشيفرة الخاصة بنا أكثر قابلية لإعادة الاستخدام. تعريف واجهة Interface بعد أن عرّفنا نوعًا جديدًا مع سلوك مُحدد، سنرى كيف يمكننا استخدام هذا السلوك، لكن قبل ذلك سنلقي نظرةً على ما سنحتاج إلى فعله إذا أردنا استدعاء التابع String من نوع Article داخل دالة: package main import "fmt" type Article struct { Title string Author string } func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } func main() { a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } Print(a) } func Print(a Article) { fmt.Println(a.String()) } أضفنا في هذه الشيفرة دالةً جديدةً تسمى Print، تأخذ وسيطًا من النوع Article. لاحظ أن كل ما تفعله هذه الدالة هو أنها تستدعي التابع String، لذا يمكننا بدلًا من ذلك تعريف واجهة للتمرير إلى الدالة: package main import "fmt" type Article struct { Title string Author string } func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } type Stringer interface { String() string } func main() { a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } Print(a) } func Print(s Stringer) { fmt.Println(s.String()) } ننشئ هنا واجهةً اسمها Stringer: ... type Stringer interface { String() string } ... تتضمّن هذه الواجهة تابعًا وحيدًا يسمى ()String يعيد سلسلةً، ويُعرّف هذا التابع على نوع بيانات مُحدد، وعلى عكس الدوال فلا يمكن له أن يُستدعى إلا من متغير من هذا النوع. نعدّل بعد ذلك بصمة الدالة Print بحيث تستقبل وسيطًا من النوع Stringer الذي يُمثّل واجهةً، وليس نوعًا مُحددًا مثل Article. بما أن المصرّف يعرف أن Stringer هي واجهة تمتلك التابع String، فلن يقبل إلا الأنواع التي تُحقق هذا التابع. يمكننا الآن استخدام الدالة Print مع أي نوع يتوافق مع الواجهة Stringer. دعنا ننشئ نوعًا آخر لتوضيح ذلك: package main import "fmt" type Article struct { Title string Author string } func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } type Book struct { Title string Author string Pages int } func (b Book) String() string { return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author) } type Stringer interface { String() string } func main() { a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } Print(a) b := Book{ Title: "All About Go", Author: "Jenny Dolphin", Pages: 25, } Print(b) } func Print(s Stringer) { fmt.Println(s.String()) } عرّفنا هنا نوع بيانات جديد يُسمى Book يمتلك التابع String، وبالتالي يُحقق الواجهة Stringer، وبالتالي يمكننا استخدامه مثل وسيط للدالة Print. The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages. أوضحنا إلى الآن كيفية استخدام واجهة واحدة فقط، ويمكن عمومًا أن يكون للواجهة أكثر من سلوك معرّف. سنرى فيما يلي كيف يمكننا جعل واجهاتنا أكثر تنوعًا من خلال التصريح عن المزيد من التوابع. تعدد السلوكيات في الواجهة تُعد كتابة أنواع صغيرة وموجزة وتركيبها في أنواع أكبر وأكثر تعقيدًا من الأمور الجيدة عند كتابة الشيفرات في لغة جو، وينطبق الشيء نفسه عند إنشاء واجهات. لمعرفة كيفية إنشاء الواجهة، سنبدأ أولًا بتعريف واجهة واحدة فقط. سنحدد شكلين؛ دائرة Circle ومربع Square، وسيُعرّف كلاهما تابعًا يُسمى المساحة Area يعيد المساحة الهندسية للشكل: package main import ( "fmt" "math" ) type Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * math.Pow(c.Radius, 2) } type Square struct { Width float64 Height float64 } func (s Square) Area() float64 { return s.Width * s.Height } type Sizer interface { Area() float64 } func main() { c := Circle{Radius: 10} s := Square{Height: 10, Width: 5} l := Less(c, s) fmt.Printf("%+v is the smallest\n", l) } func Less(s1, s2 Sizer) Sizer { if s1.Area() < s2.Area() { return s1 } return s2 } بما أن النوعين يُصرحان عن تابع Area، يمكننا إنشاء واجهة تحدد هذا السلوك. نُنشئ واجهة Sizer التالية: ... type Sizer interface { Area() float64 } … نعرّف بعد ذلك دالةً تسمى Less تأخذ واجهتين Sizer مثل وسيطين وتعيد أصغر واحدة: ... func Less(s1, s2 Sizer) Sizer { if s1.Area() < s2.Area() { return s1 } return s2 } ... لاحظ أن معاملات الدالة وكذلك القيمة المُعادة هي من النوع Sizer، وهذا يعني أننا لا نعيد مربعًا أو دائرة، بل نعيد واجهة Sizer. والآن نطبع الوسيط الذي لديه أصغر مساحة: {Width:5 Height:10} is the smallest سنضيف الآن سلوكًا آخر لكل نوع، وسنضيف التابع ()String الذي يعيد سلسلةً نصيةً، وهذا بدوره سيؤدي إلى تحقيق الواجهة fmt.Stringer: package main import ( "fmt" "math" ) type Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * math.Pow(c.Radius, 2) } func (c Circle) String() string { return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius) } type Square struct { Width float64 Height float64 } func (s Square) Area() float64 { return s.Width * s.Height } func (s Square) String() string { return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height) } type Sizer interface { Area() float64 } type Shaper interface { Sizer fmt.Stringer } func main() { c := Circle{Radius: 10} PrintArea(c) s := Square{Height: 10, Width: 5} PrintArea(s) l := Less(c, s) fmt.Printf("%v is the smallest\n", l) } func Less(s1, s2 Sizer) Sizer { if s1.Area() < s2.Area() { return s1 } return s2 } func PrintArea(s Shaper) { fmt.Printf("area of %s is %.2f\n", s.String(), s.Area()) } بما أن النوعين Circle و Square ينفذّان التابعين Area و String، سيكون بالإمكان إنشاء واجهة أخرى لوصف تلك المجموعة الأوسع من السلوك. لأجل ذلك سننشئ واجهةً تسمى Shaper من واجهة Sizer وواجهة fmt.Stringer: ... type Shaper interface { Sizer fmt.Stringer } ... ملاحظة: حبذا أن ينتهي اسم الواجهة بالحرفين er مثل fmt.Stringer و io.Writer، إلخ. ولهذا السبب أطلقنا على واجهتنا اسم Shaper، وليس Shape. يمكننا الآن إنشاء دالة تسمى PrintArea تأخذ وسيطًا من النوع Shaper. هذا يعني أنه يمكننا استدعاء التابعين على القيمة التي تُمرّر لكل من Area و String: ... func PrintArea(s Shaper) { fmt.Printf("area of %s is %.2f\n", s.String(), s.Area()) } ستحصل عند تشغيل البرنامج على الخرج التالي: area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest رأينا كيف يمكننا إنشاء واجهات أصغر وبناء واجهات أكبر حسب الحاجة. كان بإمكاننا البدء بالواجهة الأكبر وتمريرها إلى جميع الدوال، إلا أن الممارسات الجيدة تقتضي إرسال أصغر واجهة فقط إلى الدالة المطلوبة، إذ أن ذلك ضروري لجعل الشيفرة واضحة أكثر، فإذا كانت الدالة تهدف لإجراء سلوك محدد، وهذا السلوك مُعرّف في واجهة أصغر، فحبذا أن تُمرر الواجهة الأصغر وليس الواجهة الأكبر (التي تحتوي الواجهة الأصغر). إذا مررنا مثلًا الواجهة Shaper إلى الدالة Less، فهنا نفترض أنها ستستدعي كلًا من التابعين Area و String، لكنها لا تستدعي إلا التابع Area، وهذا سيجعل الدالة أقل وضوحًا، كما أننا نعلم أنه يمكننا فقط استدعاء التابع Area لأي وسيط يُمرّر إليه. خاتمة تعلمنا في هذا المقال كيفية إنشاء واجهات صغيرة وكيفية توسيعها لتصبح واجهات أكبر، وكيفية مشاركة الأشياء التي نريدها فقط مع دالة أو تابع من خلال تلك الواجهات. تعلمنا أيضًا كيفية تركيب واجهة من واجهات أصغر أو من واجهات موجودة في حزم أخرى وليس فقط الحزمة التي نعمل ضمنها. ترجمة -وبتصرف- للمقال How To Use Interfaces in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: استخدام وسوم البنية Struct Tags في لغة جو Go تعريف التوابع Methods في لغة جو Go البنى Structs في لغة جو Go
  6. تُستخدم البنى structs لجمع أجزاء متعددة من المعلومات معًا ضمن كتلة واحدة، وتُستخدم مجموعات المعلومات هذه لوصف المفاهيم ذات المستوى الأعلى، مثل العنوان المكوَّن من شارع ومدينة وولاية ورمز بريدي. عندما تقرأ هذه المعلومات من أنظمة، مثل قواعد البيانات، أو واجهات برمجة التطبيقات API، يمكنك استخدام وسوم البنية للتحكم في كيفية تخصيص هذه المعلومات لحقول البنية. وسوم البنية هي أجزاء صغيرة من البيانات الوصفية المرفقة بحقول البنية التي توفر إرشادات إلى شيفرة جو أُخرى تعمل مع البنية. كيف يبدو شكل وسم البنية؟ وسم البنية في لغة جو هو "توضيح" يُكتب بعد نوع الحقل داخل البنية، ويتكون كل وسم من زوج "key:"value أي مفتاح مرتبط بقيمة مقابلة ويوضع ضمن علامتين اقتباس مائلة (`) كما يلي: type User struct { Name string `example:"name"` } من خلال هذا التعريف، ستكون هناك شيفرة جو أخرى قادرة على فحص هذه البنية واستخراج القيم المخصصة لمفاتيح معينة، وبدون هذه الشيفرة الأخرى لن تؤثر وسوم البنية على تعليماتك البرمجية. جرّب هذا المثال لترى كيف يبدو وسم البنية، وكيف سيكون عديم التأثير دون الشيفرة الأخرى. package main import "fmt" type User struct { Name string `example:"name"` } func (u *User) String() string { return fmt.Sprintf("Hi! My name is %s", u.Name) } func main() { u := &User{ Name: "Sammy", } fmt.Println(u) } سيكون الخرج على النحو التالي: Hi! My name is Sammy يعرّف هذا المثال نوع بيانات باسم User يمتلك حقلًا اسمه Name، وأعطينا هذا الحقل وسم بنية "example:"name مُتمثّل بالمفتاح example والقيمة "name" للحقل Name. عرّفنا التابع ()String في البنية User، والذي تحتاجه الواجهة fmt.Stringer، وبالتالي سيُستدعى تلقائيًا عندما نُمرّر هذا النوع إلى fmt.Println وبالتالي نحصل على طباعة مُرتبة للبنية. نأخذ في الدالة main متغيرًا من النوع User ونمرّره إلى دالة الطباعة fmt.Println. على الرغم من أنه لدينا وسم بنية، إلا أننا لن نلحظ أي فرق في الخرج، أي كما لو أنها غير موجودة. لكي نحصل على تأثير وسم البنية، يجب كتابة شيفرة أخرى تفحص البنية في وقت التشغيل runtime. تحتوي المكتبة القياسية على حزم تستخدم وسوم البنية كأنها جزء من عملها، وأكثرها شيوعًا هي حزمة encoding/json. دورة تطوير واجهات المستخدم ابدأ عملك الحر بتطوير واجهات المواقع والمتاجر الإلكترونية فور انتهائك من الدورة اشترك الآن ترميز JSON جسون JSON هي اختصار إلى "ترميز كائن باستخدام جافا سكريبت JavaScript Object Notation"، وهي تنسيق نصي لترميز مجموعات البيانات المنظمة وفق أسماء مفاتيح مختلفة. يُشاع استخدام جسون لربط البيانات بين البرامج المختلفة، إذ أن التنسيق بسيط ويوجد مكتبات جاهزة لفك ترميزه في العديد من اللغات البرمجية. فيما يلي مثال على جسون: { "language": "Go", "mascot": "Gopher" } يتضمن كائن جسون أعلاه مفتاحين؛ الأول language والثاني mascot، ولكل منهما قيمة مرتبطة به هما Go و Gopher. يستخدم مُرمّز encoder جسون في المكتبة القياسية وسوم البنية مثل "توصيفات annotations" تشير إلى الكيفية التي تريد بها تسمية الحقول الخاصة بك في خرج جسون. يمكن العثور على آليات ترميز وفك ترميز جسون في حزمة encoding/json من هنا. جرب هذا المثال لترى كيف يكون ترميز جسون دون وسوم البنية: package main import ( "encoding/json" "fmt" "log" "os" "time" ) type User struct { Name string Password string PreferredFish []string CreatedAt time.Time } func main() { u := &User{ Name: "Sammy the Shark", Password: "fisharegreat", CreatedAt: time.Now(), } out, err := json.MarshalIndent(u, "", " ") if err != nil { log.Println(err) os.Exit(1) } fmt.Println(string(out)) } وسيكون الخرج على النحو التالي: { "Name": "Sammy the Shark", "Password": "fisharegreat", "CreatedAt": "2019-09-23T15:50:01.203059-04:00" } عرّفنا بنية تمثّل مُستخدم مع حقول تُدل على اسمه Name وكلمة المرور Password وتاريخ إنشاء الحساب CreatedAt. أخذنا داخل الدالة main متغيرًا من هذه البنية وأعطينا قيمًا لجميع حقوله عدا PreferredFish (يحب Sammy جميع الأسماك). بعد ذلك، مرّرنا البنية إلى الدالة json.MarshalIndent، إذ تمكننا هذه الدالة من رؤية خرج جسون بسهولة ودون استخدام أي أداة خارجية. يمكن استبدال الاستدعاء السابق لهذه الدالة بالاستدعاء (json.Marshal(u وذلك لطباعة جسون بدون أي فراغات إضافية، إذ يتحكم الوسيطان الإضافيان للدالة json.MarshalIndent في بادئة الخرج، التي أهملناها مع السلسلة الفارغة، وبالمحارف المُراد استخدامها من أجل تمثيل المسافة البادئة (هنا فراغين). سُجّلت الأخطاء الناتجة عن json.MarshalIndent وأُنهي البرنامج باستخدام (os.Exit(1، وأخيرًا حوّلنا المصفوفة byte[] المُعادة من json.MarshalIndent إلى سلسلة ومررنا السلسلة الناتجة إلى fmt.Println للطباعة على الطرفية. تظهر حقول البنية تمامًا كما تُسمّى، وهذا ليس طبعًا نمط جسون النموذجي الذي يستخدم تنسيق "سنام الجمل Camel case" لأسماء الحقول. سنحقق ذلك في المثال التالي، إذ سنرى عند تشغيله أنه لن يعمل لأن أسماء الحقول المطلوبة تتعارض مع قواعد جو المتعلقة بأسماء الحقول المصدَّرة. package main import ( "encoding/json" "fmt" "log" "os" "time" ) type User struct { name string password string preferredFish []string createdAt time.Time } func main() { u := &User{ name: "Sammy the Shark", password: "fisharegreat", createdAt: time.Now(), } out, err := json.MarshalIndent(u, "", " ") if err != nil { log.Println(err) os.Exit(1) } fmt.Println(string(out)) } وسيكون الخرج على النحو التالي: {} استبدلنا هذه المرة أسماء الحقول، بحيث تتوافق مع تنسيق سنام الجمل؛ فبدلًا من Name وضعنا name وبدلًا من Password وضعنا password وبدلًا من CreatedAt وضعنا createdAt، وعدّلنا داخل متن الدالة main أسماء الحقول أيضًا بحيث تتوافق مع الأسماء الجديدة، ومرّرنا البنية إلى الدالة json.MarshalIndent كما في السابق. الخرج كان عبارة عن كائن جسون فارغ {}. يفرض نمط سنام الجمل أن يكون أول حرف صغير دومًا، بينما لا تهتم جسون بذلك، ولغة جو صارمة مع حالة اﻷحرف؛ إذ تدل البداية بحرف كبير على أن الحقل غير مُصدّر وتدل البداية بحرف صغير على أنّه مُصدّر، وبما أن الحزمة encoding/json هي حزمة منفصلة عن حزمة main التي نستخدمها، يجب علينا كتابة الحرف الأول بأحرف كبيرة لجعله مرئيًا للحزمة encoding/json. حسنًا، يبدو أننا في طريق مسدود، فنحن بحاجة إلى طريقة ما لننقل إلى ترميز جسون ما نود تسمية هذا الحقل به. استخدام وسوم البنية للتحكم بالترميز يمكنك تعديل المثال السابق، بحيث تكون قادرًا على تصدير الحقول المتوافقة مع تنسيق سنام الجمل عن طريق إضافة وسم بنية لكل حقل. يجب أن يكون لدى وسوم البنية التي تتعرّف عليها الحزمة encoding/json مفتاحها json مع قيمة مُرافقة لهذا المفتاح تتحكم بالخرج. إذًا، من خلال استخدام تنسيق سنام الجمل لقيم المفاتيح، سيستخدم المرمّز هذه القيم مثل أسماء للحقول مع الإبقاء على الحقول مُصدّرة، وبالتالي نكون أنهينا المشاكل السابقة: package main import ( "encoding/json" "fmt" "log" "os" "time" ) type User struct { Name string `json:"name"` Password string `json:"password"` PreferredFish []string `json:"preferredFish"` CreatedAt time.Time `json:"createdAt"` } func main() { u := &User{ Name: "Sammy the Shark", Password: "fisharegreat", CreatedAt: time.Now(), } out, err := json.MarshalIndent(u, "", " ") if err != nil { log.Println(err) os.Exit(1) } fmt.Println(string(out)) } سيكون الخرج على النحو التالي: { "name": "Sammy the Shark", "password": "fisharegreat", "preferredFish": null, "createdAt": "2019-09-23T18:16:17.57739-04:00" } لاحظ أننا تركنا أسماء الحقول تبدأ بأحرف كبيرة من أجل السماح بعملية التصدير، ولحل مشكلة تنسيق سنام الجمل استخدمنا وسوم بنية من الشكل "json:"name، إذ أن "name" هي القيمة التي نريد من json.MarshalIndent أن تعرضها عند طباعة كائن جسون. لاحظ أن الحقل PreferredFish لم نُعطه أي قيمة، وبالتالي قد لا نرغب بظهوره عند طباعة كائن جسون. فيما يلي سنتحدث حول هذا الموضوع. حذف حقول جسون الفارغة يُعد حذف حقول الخرج التي ليس لها قيمة في جسون أمرًا شائعًا، وبما أن جميع الأنواع في جو لها "قيمة صفرية" أو قيمة افتراضية مُهيّأة بها، تحتاج حزمة encoding/json إلى معلومات إضافية لتتمكن من معرفة أن بعض الحقول ينبغي عدّها غير مضبوطة، أي قيمتها صفرية في جو. هذه المعلومات هي في الحقيقة مجرد كلمة واحدة نضيفها إلى نهاية القيمة المرتبطة بمفتاح الحقل في وسم البنية؛ وهذه الكلمة هي omitempty,، إذ نُخبر جسون من خلال إضافة هذه الكلمة إلى الحقل بأننا لا نريد ظهوره عندما تكون قيمته صفرية. يوضح المثال التالي الأمر: package main import ( "encoding/json" "fmt" "log" "os" "time" ) type User struct { Name string `json:"name"` Password string `json:"password"` PreferredFish []string `json:"preferredFish,omitempty"` CreatedAt time.Time `json:"createdAt"` } func main() { u := &User{ Name: "Sammy the Shark", Password: "fisharegreat", CreatedAt: time.Now(), } out, err := json.MarshalIndent(u, "", " ") if err != nil { log.Println(err) os.Exit(1) } fmt.Println(string(out)) } سيكون الخرج على النحو التالي: { "name": "Sammy the Shark", "password": "fisharegreat", "createdAt": "2019-09-23T18:21:53.863846-04:00" } عدّلنا الأمثلة السابقة بحيث أصبح حقل PreferredFish يحتوي على وسم البنية "json:"preferredFish,omitempty، إذ سيمنع وجود الكلمة omitempty, ظهور هذا الحقل في خرج كائن جسون. أصبحت الآن الأمور أفضل، لكن هناك مشكلة أخرى واضحة، وهي ظهور كلمة المرور، ولحل المشكلة تؤمن encoding/json طريقةً لتجاهل الحقول الخاصة تمامًا. منع عرض الحقول الخاصة في خرج كائنات جسون يجب تصدير بعض الحقول من البنى حتى تتمكن الحزم الأخرى من التفاعل بطريقة صحيحة مع النوع، لكن قد تكون المشكلة في حساسية طبيعة أحد هذه الحقول كما في حالة كلمة المرور في المثال السابق، لذا نود أن يتجاهل مُرمّز جسون الحقل تمامًا، حتى عند تهيئته بقيمة. يكون حل هذه المشكلة باستخدام المحرف - ليكون قيمةً لوسيط المفتاح الخاص بوسم البنية :json. يعمل هذا المثال على إصلاح مشكلة عرض كلمة مرور المستخدم. package main import ( "encoding/json" "fmt" "log" "os" "time" ) type User struct { Name string `json:"name"` Password string `json:"-"` CreatedAt time.Time `json:"createdAt"` } func main() { u := &User{ Name: "Sammy the Shark", Password: "fisharegreat", CreatedAt: time.Now(), } out, err := json.MarshalIndent(u, "", " ") if err != nil { log.Println(err) os.Exit(1) } fmt.Println(string(out)) } سيكون الخرج على النحو التالي: { "name": "Sammy the Shark", "createdAt": "2019-09-23T16:08:21.124481-04:00" } الشيء الوحيد الذي تغير في هذا المثال عن المثال السابق هو أن حقل كلمة المرور يستخدم الآن القيمة الخاصة "-" لوسم البنية :json. يمكنك أن تلاحظ من الخرج السابق اختفاء كلمة المرور من الخرج. ميزتا التجاهل والإخفاء، أو حتى باقي الخيارات في حزمة encoding/json التي استخدمناها مع حقل PreferredFish و Password، ليستا قياسيتين، أي ليست كل الحزم تستخدم نفس الميزات ونفس بنية القواعد، لكن حزمة encoding/json هي حزمة مُضمّنة عمومًا في المكتبة القياسية، وبالتالي سيكون لدى الحزم الأخرى نفس الميزات ونفس العرض convention. مع ذلك، من المهم قراءة التوثيق الخاص بأي حزمة تابعة لجهة خارجية تستخدم وسوم البنية لمعرفة ما هو مدعوم وما هو غير مدعوم. خاتمة توفر وسوم البنية وسيلةً قويةً لتسهيل التعامل مع دوال الشيفرات التي تعمل مع البنى، كما توفر العديد من الحزم القياسية والخارجية طرقًا لتخصيص عملياتها من خلال استخدام هذه الوسوم. يوفر استخدام الوسوم بفعالية في التعليمات البرمجية الخاصة بك سلوك تخصيص ممتاز ويوثّق بإيجاز كيفية استخدام هذه الحقول للمطوّرين المستقبليين. ترجمة -وبتصرف- للمقال How To Use Struct Tags in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: بناء البرامج المكتوبة بلغة جو Go وتثبيتها استخدام وسوم البناء لتخصيص الملفات التنفيذية Binaries في لغة جو Go
  7. استخدمنا خلال هذه السلسلة البرمجة بلغة GO الأمر go run كثيرًا، وكان الهدف منه تشغيل شيفرة البرنامج الخاص بنا، إذ أنه يُصرّف شفرة المصدر تلقائيًا ويُشغّل الملف التنفيذي الناتج أي القابل للتنفيذ executable. يُعد هذا الأمر مفيدًا عندما تحتاج إلى اختبار برنامجك من خلال سطر الأوامر command line، لكن عندما ترغب بنشر تطبيقك، سيتطلب منك ذلك بناء تعليماتك البرمجية في ملف تنفيذي، أو ملف واحد يحتوي على شيفرة محمولة أو تنفيذية (يُطلق عليه أيضًا الكود-باء p-code، وهو شكل من أشكال مجموعة التعليمات المصممة للتنفيذ الفعّال بواسطة مُصرّف برمجي) يمكنها تشغيل تطبيقك. يمكنك لإنجاز ذلك استخدام سلسلة أدوات جو لبناء البرنامج وتثبيته. تسمى عملية ترجمة التعليمات البرمجية المصدر إلى ملف تنفيذي في لغة جو بالبناء؛ فعند بناء هذا الملف التنفيذي ستُضاف إليه الشيفرة اللازمة لتنفيذ البرنامج التنفيذي الثنائي binary على النظام الأساسي المُستهدف. هذا يعني أن جو التنفيذي Go binary (خادم مفتوح المصدر أو حزمة برمجية تسمح للمستخدمين الذين لا يستخدمون جو بتثبيت الأدوات المكتوبة بلغة جو بسرعة، دون تثبيت مُصرّف جو أو مدير الحزم - كل ما تحتاجه هو curl) لا يحتاج إلى اعتماديات dependencies النظام مثل أدوات جو للتشغيل على نظام جديد. سيسمح وضع هذه الملفات التنفيذية ضمن مسار ملف تنفيذي على نظامك، بتشغيل البرنامج من أي مكان في نظامك؛ أي كما لو أنك تُثبّت أي برنامج عادي على نظام التشغيل الخاص بك. ستتعلم في هذا المقال كيفية استخدام سلسلة أدوات لغة جو Go toolchain لتشغيل وبناء وتثبيت برنامج "!Hello، World" لفهم كيفية استخدام التطبيقات البرمجية وتوزيعها ونشرها بفعالية. المتطلبات أن يكون لديك مساحة عمل خاصة في لغة جو، وإذا لم يكن لديك ذلك اتبع سلسلة المقالات التالية، فقد تحدّثنا عن ذلك في بداية السلسلة "البرمجة بلغة Go: تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu تثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك أو إس macOS تثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز. إعداد وتشغيل جو التنفيذي Go Binary سننشئ بدايةً تطبيقًا بسيطًا بلغة جو يطبع العبارة الترحيبية "!Hello، World"، وذلك لتوضيح سلسلة أدوات جو، وانظر مقال كتابة برنامجك الأول في جو Go إن لم تتطلع عليه مسبقًا. أنشئ مجلد "greeter" داخل المجلد "src": $ mkdir greeter بعد ذلك أنشئ ملف "main.go" بعد الانتقال إلى هذا المجلد الذي أنشأته للتو، ويمكنك استخدام أي محرر نصوص تختاره ﻹنشاء الملف، هنا استخدمنا محرر نانو nano: $ cd greeter $ nano main.go بعد فتح الملف، ضِف المحتويات التالية إليه: package main import "fmt" func main() { fmt.Println("Hello, World!") } عند تشغيل هذا البرنامج سيطبع العبارة "!Hello, World" ثم سينتهي البرنامج. احفظ الملف الآن واغلقه. استخدم الأمر go run لاختبار البرنامج: $ go run main.go سيكون الخرج على النحو التالي: Hello, World! كما تحدّثنا سابقًا؛ يبني الأمر go run الملف المصدري الخاص بك في ملف تنفيذي، ثم يُصرّفه ويعرض الناتج. نريد هنا أن نتعلم كيفية بناء الملف التنفيذي بطريقة تمكننا من توزيع ونشر برامجنا، ولذلك سنستخدم الأمر go build في الخطوة التالية. إنشاء وحدة جو من أجل Go binary بُنيت برامج ومكتبات جو وفق المفهوم الأساسي "للوحدة module"، إذ تحتوي الوحدة على معلومات حول المكتبات التي يستخدمها برنامجك وإصدارات هذه المكتبات التي يجب استخدامها. لتخبر جو أن مجلدًا ما هو وحدة، ستحتاج إلى إنشاء هذا المجلد باستخدام الأمر go mod: $ go mod init greeter سيؤدي ذلك إلى إنشاء الملف go.mod الذي يتضمن اسم الوحدة ونسخة جو المستخدمة في إنشائها. go: creating new go.mod: module greeter go: to add module requirements and sums: go mod tidy سيطالبك جو بتشغيل go mod tidy لتحديث متطلبات هذه الوحدة إذا تغيرت في المستقبل، ولن يكون لتشغيله الآن أي تأثير إضافي. بناء الملفات التنفيذية باستخدام الأمر go build يمكنك بناء ملف تنفيذي باستخدام الأمر go build من أجل تطبيق مكتوب بلغة جو، مما يسمح لك بتوزيعه ونشره في المكان الذي تريده. سنجرب ذلك مع ملف main.go. من داخل المجلد greeter من خلال تنفيذ الأمر التالي: $ go build إذا لم تُقدم وسيطًا لهذا الأمر، سيُصرّف الأمر go build تلقائيًا برنامج main.go في مجلدك الحالي، وسيتضمن ذلك أيضًا كل الملفات التي يكون امتدادها go.* في هذا المجلد. سيبني أيضًا جميع التعليمات البرمجية الداعمة واللازمة لتكون قادرًا على تنفيذ البرنامج التنفيذي على أي جهاز حاسوب له نفس معمارية النظام، بغض النظر عما إذا كان هذا النظام يحتوي على أدوات جو أو مُصرّف جو أو ملفاته المصدرية. إذًا، فقد بنيت تطبيق الترحيب الخاص بك في ملف تنفيذي أُضيف إلى مجلدك الحالي. تحقق من ذلك عن طريق تشغيل الأمر التالي: $ ls إذا كنت تستخدم نظام ماك أو إس macOS أو لينكس Linux، فستجد ملفًا تنفيذيًا جديدًا مُسمّى على اسم المجلد الذي بنيت فيه برنامجك: greeter main.go go.mod ملاحظة: في نظام التشغيل ويندوز، سيكون الملف التنفيذي باسم "greeter.exe". سيُنشئ الأمرgo build افتراضيًا ملفًا تنفيذيًا للنظام الأساسي والمعمارية الحاليين. على سبيل المثال، إذا بُنيَ على نظام تشغيل linux / 386، سيكون الملف التنفيذي متوافقًا مع أي نظام Linux / 386 آخر، حتى إذا لم يكن جو مُثبّتًا على ذلك النظام. تدعم لغة جو إمكانية البناء على الأنظمة والمعماريات الأخرى. بعد أن أنشأت ملفك التنفيذي، يمكنك تشغيله للتأكد من أنه قد بُنيَ بطريقة سليمة. في نظام ماك أو إس أو لينكس، شغّل الأمر التالي: $ ./greeter أما في ويندوز نفّذ الأمر: $ greeter.exe سيكون الخرج على النحو التالي: Hello, World! بذلك تكون قد أنشأت ملفًا تنفيذيًّا يحتوي على برنامجك وعلى شيفرة النظام المطلوبة لتشغيل هذا الملف التنفيذي. يمكنك الآن توزيع هذا البرنامج على أنظمة جديدة أو نشره على خادم، مع العلم أن الملف سيعمل دائمًا على نفس البرنامج. سنشرح فيما يلي كيفية تسمية ملف تنفيذي وكيفية تعديله بحيث يمكنك التحكم أكثر في عملية بناء البرنامج. تغيير اسم الملف التنفيذي بعد أن عرفت كيفية إنشاء ملف تنفيذي، ستكون الخطوة التالية هي تحديد كيفية اختيار جو اسمًا للملف التنفيذي وكيفية تخصيص هذا الاسم لمشروعك. يقرر جو تلقائيًا اسم الملف التنفيذي الذي بُنيَ عند تشغيل الأمر go build، وذلك اعتمادًا على الوحدة التي أنشأتها. عندما نفّذنا الأمر go mod init greeter منذ قليل، أُنشئت وحدة باسم greeter، وهذا هو سبب تسمية الملف التنفيذي الثنائي binary الذي أُنشئ باسم "greeter" بدوره. إذا فتحت ملف go.mod (الذي يُفترض أن يكون ضمن مجلد مشروعك)، وكان يتضمن التصريح التالي: module github.com/sammy/shark وهذا يعني أن الاسم الافتراضي للملف التنفيذي الذي بُنيَ هو shark. لن تكون هذه التسميات الافتراضية دائمًا الخيار الأفضل لتسمية الملف التنفيذي الخاص بك في البرامج الأكثر تعقيدًا التي تتطلب تسميات اصطلاحية محددة، وسيكون من الأفضل تحديد أسماء مُخصصة من خلال الراية o-. سنغيّر الآن اسم الملف التنفيذي الذي أنشأناه في القسم السابق إلى الاسم "hello" ونضعه في مجلد فرعي يسمى "bin". لن نحتاج إلى إنشاء هذا المجلد (bin)، إذ ستتكفل جو بذلك أثناء عملية البناء. نفّذ الأمر go build مع الرايةo-: $ go build -o bin/hello تُخبر الراية o- مُصرّف جو أن عليه مُطابقة خرج الأمر go build مع الوسيط المُحدد بعدها والمتمثّل بالعبارة bin/hello. بعبارةٍ أوضح؛ تُخبر هذه الراية المُصرّف أن الملف التنفيذي السابق يجب أن يكون اسمه "hello" وأن يكون ضمن مجلد اسمه "bin"، وفي حال لم يكن هذا المجلد موجودًا فعليك إنشاؤه تلقائيًّا. لاختبار الملف التنفيذي الجديد، انتقل إلى المجلد الجديد وشغّل الملف التنفيذي: $ cd bin $ ./hello ستحصل على الخرج التالي: Hello, World! يمكنك الآن اختيار اسم الملف التنفيذي ليناسب احتياجات مشروعك. إلى الآن لا تزال مقيدًا بتشغيل ملفك التنفيذي من المجلد الحالي، وإذا أردت استخدام الملفات التنفيذية التي بنيتها من أي مكان على نظامك، يجب عليك تثبيتها باستخدام الأمر go install. تثبيت برامج جو باستخدام الأمر go install ناقشنا حتى الآن كيفية إنشاء ملفات تنفيذية من ملفات مصدرية بامتداد "go."، هذه الملفات التنفيذية مفيدة من أجل التوزيع والنشر والاختبار، ولكن لا يمكن تنفيذها من خارج المجلدات المصدرية الموجودة ضمنها. قد تكون هذه مشكلة إذا كنت تريد استخدام برنامجك باستمرار ضمن سكريبتات الصدفة shell scripts أو في مهام أخرى. لتسهيل استخدام البرامج، يمكنك تثبيتها في نظامك والوصول إليها من أي مكان. لتوضيح الفكرة سنستخدم الأمر go install لتثبيت البرنامج الذي نعمل عليه. يعمل الأمر go install على نحوٍ مماثل تقريبًا للأمر go build، ولكن بدلًا من ترك الملف التنفيذي في المجلد الحالي أو مجلد محدد بواسطة الراية o-، فإنه يضعه في المجلد "GOPATH/bin$". لمعرفة مكان وجود مجلد "GOPATH$" الخاص بك، شغّل الأمر التالي: $ go env GOPATH قد يختلف الخرج الذي تتلقاه، ولكن يُفترض أن يكون ضمن مجلد go الموجود داخل مجلد HOME$: $HOME/go نظرًا لأن go install سيضع الملفات التنفيذية التي أُنشئت في مجلد فرعي للمجلد GOPATH$ اسمه bin، يجب إضافة هذا المجلد إلى متغير البيئة PATH$. تحدّثنا عن هذه المواضيع في مقالة كيفية تثبيت جو وإعداد بيئة برمجة محلية. بعد إعداد المجلد GOPATH/bin$، ارجع إلى مجلد greeter: $ cd .. شغّل الآن أمر التثبيت: $ go install سيؤدي هذا إلى بناء ملفك التنفيذي ووضعه في GOPATH/bin$. شغّل الأمر التالي لاختبار ذلك: $ ls $GOPATH/bin سيسرد لك هذا الأمر محتويات المجلد GOPATH/bin$: greeter ملاحظة: لا يدعم الأمر go install الراية o-، لذلك سيستخدم الاسم الافتراضي الذي تحدّثنا عنه سابقًا لتسمية الملف التنفيذي. تحقق الآن ما إذا كان البرنامج سيعمل من خارج المجلد المصدر. ارجع أولًا إلى المجلد HOME: $ cd $HOME استخدم ما يلي لتشغيل البرنامج: $ greeter ستحصل على الخرج التالي: Hello, World! يمكنك الآن تثبيت البرامج التي تكتبها في نظامك، مما يتيح لك استخدامها من أي مكان، ومتى احتجت إليها. خاتمة أوضحنا في هذا المقال كيف تسهل سلسلة أدوات جو عملية إنشاء ملفات تنفيذية ثنائية من التعليمات البرمجية المصدرية، ويمكن توزيعها لتعمل على أنظمة أخرى، حتى لو لم تحتوي على أدوات وبيئات للغة جو. استخدمنا أيضًا الأمر go install لبناء برامجنا وتثبيتها تلقائيًا مثل ملفات تنفيذية ضمن متغير البيئة PATH$ الخاص بالنظام. ستستطيع من خلال الأمرين go install و go build مشاركة واستخدام التطبيق الخاص بك كما تشاء. الآن بعد أن تعرفت على أساسيات go build، يمكنك استكشاف كيفية إنشاء شيفرة مصدر معيارية من خلال المقالة استخدام وسوم البناء لتخصيص الملفات التنفيذية في لغة جو Go. ترجمة -وبتصرف- للمقال How To Build and Install GOPrograms لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: تعريف التوابع Methods في لغة جو Go كتابة برنامجك الأول في جو Go
  8. تُمكّنك الدوال Functions من تنظيم منطق مُحدد ضمن إجراءات procedures قابلة للتكرار يمكنها استخدام وسطاء مختلفة في كل مرة تُنفّذ فيها. تعمل عدة دوال غالبًا على نفس الجزء من البيانات في كل مرة. يُمكن لغة جو التعرّف على هذه الأنماط، وتسمح لك بتعريف دوال خاصة تُسمى "التوابع Methods"، وتجري عمليات على نوع بيانات مُحدد يُشار إليه باسم "المُستقبل Receiver". تعريف تابع قواعد كتابة التوابع مُشابهة لقواعد كتابة الدوال، والاختلاف الوحيد هو إضافة معامل يُوضع بعد الكلمة المفتاحية func لتحديد مُستقبل التابع. المُستقبل هو تصريح لنوع البيانات الذي تُريد تعريف التابع من أجله. نُصرّح في المثال التالي عن تابع يُطبّق على نوع بنية struct: package main import "fmt" type Creature struct { Name string Greeting string } func (c Creature) Greet() { fmt.Printf("%s says %s", c.Name, c.Greeting) } func main() { sammy := Creature{ Name: "Sammy", Greeting: "Hello!", } Creature.Greet(sammy) } نحصل عند تشغيل الشيفرة على ما يلي: Sammy says Hello! أنشأنا بنيةً أسميناها Creature تمتلك حقلين من النوع string باسم Name و Greeting. لدى Creature تابع وحيد مُعرّف يُسمى Greet. أسندنا أثناء التصريح عن المستقبل نسخةً من Creature إلى المتغير c، بحيث يُمكننا من خلالها الوصول إلى حقول Creature وطباعة محتوياته باستخدام دالة fmt.Printf. يُشار عادةً للمُستقبل في لغات البرمجة الأخرى بكلمات رئيسية مثل self كما في لغة بايثون أو this كما في لغة جافا. يُعد المستقبل في لغة جو مُتغيرًا كما باقي المتغيرات، وبالتالي يُمكنك تسميته كما تشاء. يُفضّل عمومًا تسميته بأول حرف من اسم نوع البيانات الذي تُريد أن يتعامل معه التابع، فقد سُمّي المعامل في المثال السابق بالاسم c لأن نوع بيانات المستقبل كان Creature. أنشأنا داخل الدالة main نسخةً من البنية Creature وأعطينا حقولها Name و Greeting القيم "Sammy" و "!Hello" على التوالي. استدعينا التابع Greet والذي أصبح مُعرّفًا على نوع البيانات Creature من خلال وضع نقطة Creature.Greet، ومرّرنا له النسخة التي أنشأناه للتو مثل وسيط. قد يبدو هذا الأسلوب في الاستدعاء مُربكًا قليلًا، سنعرض فيما يلي أسلوبًا آخر لاستدعاء التوابع: package main import "fmt" type Creature struct { Name string Greeting string } func (c Creature) Greet() { fmt.Printf("%s says %s", c.Name, c.Greeting) } func main() { sammy := Creature{ Name: "Sammy", Greeting: "Hello!", } sammy.Greet() } ستحصل عند تشغيل الشيفرة على نفس الخرج السابق: Sammy says Hello! لا يختلف هذا المثال عن المثال السابق إلا في استدعاء التابع Greet، إذ استخدمنا هنا الأسلوب المختصر، بحيث نضع اسم المتغير الذي يُمثّل البنية Creature وهو sammy متبوعًا بنقطة ثم اسم التابع بين قوسين ()sammy.Greet. يُحبّذ دومًا استخدام هذا الأسلوب الذي يُسمّى التدوين النقطي dot notation، فمن النادر أن يستخدم المطورون الأسلوب السابق الذي يُسمّى أسلوب الاستدعاء الوظيفي functional invocation style. يوضح المثال التالي أحد الأسباب التي تجعل هذا الأسلوب أكثر انتشارًا: package main import "fmt" type Creature struct { Name string Greeting string } func (c Creature) Greet() Creature { fmt.Printf("%s says %s!\n", c.Name, c.Greeting) return c } func (c Creature) SayGoodbye(name string) { fmt.Println("Farewell", name, "!") } func main() { sammy := Creature{ Name: "Sammy", Greeting: "Hello!", } sammy.Greet().SayGoodbye("gophers") Creature.SayGoodbye(Creature.Greet(sammy), "gophers") } عند تشغيل الشيفرة السابقة سيكون الخرج على النحو التالي: Sammy says Hello!! Farewell gophers ! Sammy says Hello!! Farewell gophers ! عدّلنا الأمثلة السابقة لكي نُقدّم تابعًا جديدًا يُسمى SayGoodbye، وعدّلنا أيضًا تعريف التابع Greet بحيث يُعيد أيضًا المُعامل c الذي يُمثل Creature، وبالتالي سيكون لدينا إمكانية استخدام القيمة المُعادة من هذا التابع والمُتمثّلة بنسخة Creature. نستدعي في الدالة main التابعين SayGoodbye و Greet على المتغير sammy باستخدام أسلوبي التدوين النقطي والاستدعاء الوظيفي. يعطي الأسلوبان نفس النتائج في الخرج، لكن التدوين النُقطي أسهل للقراءة، كما أنه يعرض لنا عملية استدعاء التوابع مثل تسلسل، إذ نستدعي التابع Greet من خلال المتغير sammy الذي يُمثّل Creature، ثم نستدعي التابع SayGoodbye من خلال القيمة التي يُعيدها هذا التابع والتي تمثّل Creature أيضًا.لا يعكس أسلوب الاستدعاء الوظيفي هذا الترتيب بسبب إضافة معامل إلى استدعاء SayGoodbye يؤدي إلى حجب ترتيب الاستدعاءات. وضوح التدوين النقطي هو السبب في أنه النمط المفضل لاستدعاء التوابع في لغة جو، سواءً في المكتبة القياسية أو في الحزم الخارجية. تعريف التوابع على نوع بيانات مُحدد هو أمر مُختلف عن تعريف الدوال التي تعمل بناءً على قيمة ما، وهذا له أهمية خاصة في لغة جو لأنه مفهوم أساسي في الواجهات interfaces. الواجهات interfaces عندما تُعرّف أي تابع على نوع بيانات في لغة جو، يضُاف هذا التابع إلى مجموعة التوابع الخاصة بهذا النوع، وهي مجموعة من الدوال المرتبطة بهذا النوع ويستخدمها مُصرّف لغة جو لتحديد إذا كان يمكن إسناد نوع ما لمتغير من نوع "واجهة interface"؛ وهذا النوع هو نهج يعتمده المُصرّف لضمان أن مُتغيّرًا من نوع البيانات المطلوب يُحقق التوابع التي تتضمنها الواجهة. يُعد أي نوع يمتلك توابع لها نفس الاسم ونفس المعلمات ونفس القيم المُعادة؛ مثل تلك الموجودة في تعريف الواجهة منفِّذًا لتلك الواجهة ويُسمح بإسناده إلى متغيرات من تلك الواجهة. نعرض فيما يلي تعريفًا لواجهة fmt.Stringer من المكتبة القياسية: type Stringer interface { String() string } لاحظ هنا استخدام الكلمة المفتاحية type لتعريف نوع بيانات جديد يُمثّل "واجهة". لكي ينفّذ أي نوع واجهة fmt.Stringer، يجب أن يوفر تابعًا اسمه ()String يُعيد سلسلة نصية. سيُمكّنك تنفيذ هذه الواجهة من طباعة "نوعك" تمامًا كما تريد ويُسمى ذلك "طباعة مُرتّبة pretty-printed"، وذلك عند تمرير نسخة من النوع الخاص بك إلى دوال محددة في حزمة fmt. يُعرّف المثال التالي نوعًا ينفّذ هذه الواجهة: package main import ( "fmt" "strings" ) type Ocean struct { Creatures []string } func (o Ocean) String() string { return strings.Join(o.Creatures, ", ") } func log(header string, s fmt.Stringer) { fmt.Println(header, ":", s) } func main() { o := Ocean{ Creatures: []string{ "sea urchin", "lobster", "shark", }, } log("ocean contains", o) } ويكون الخرج على النحو التالي: ocean contains : sea urchin, lobster, shark صرّحنا في هذا المثال عن نوع بيانات جديد يُمثّل بنيةً اسمها Ocean. يُمكننا القول أن هذه البنية تنفّذ الواجهة fmt.Stringer لأنها تعرِّف تابعًا اسمه String لا يأخذ أي وسطاء ويعيد سلسلةً نصية، أي تمامًا كما في الواجهة. عرّفنا داخل الدالة main متغيرًا o يُمثّل بنيةً Ocean ومرّرناه إلى الدالة log التي تطبع سلسة نصية تُمرّر لها، متبوعةً بأي شيء تُنفّذه وليكن fmt.Stringer. سيسمح لنا مُصرّف جو هنا أن نُمرّر البنية o لأنه يُنفّذ كل التوابع التي تطلبها الدالة fmt.Stringer (هنا يوجد تابع وحيد String). نستخدم داخل الدالة log دالة الطباعة fmt.Println التي تستدعي التابع String من البنية Ocean لأننا مررنا لها المعامل s من النوع fmt.Stringer في ترويستها (أي بمثابة أحد معاملاتها). إذا لم تنفّذ البنية Ocean التابع String سيُعطينا جو خطأً في التصريف، لأن الدالة log تتطلب تمرير وسيط من النوع fmt.Stringer، وسيكون الخطأ كما يلي: src/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log: Ocean does not implement fmt.Stringer (missing String method) سيتحقق مُصرّف لغة جو من مطابقة التابع ()String للتابع المُستدعى من قِبل الواجهة fmt.Stringer، وإذا لم يكن مُطابقًا، سيعطي الخطأ: src/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log: Ocean does not implement fmt.Stringer (wrong type for String method) have String() want String() string استخدمت التوابع المُعرّفة في الأمثلة السابقة "مُستقبل قيمة"، أي إذا استخدمنا أسلوب الاستدعاء الوظيفي للتوابع، سيكون المعامل الأول الذي يشير إلى النوع الذي عُرّف التابع عليه قيمةً من هذا النوع وليس مؤشرًا؛ وهذا يعني أن أي تعديل نُجريه على هذا النوع المُمثّل بالمعامل المُحدد (مثلًا في المثال السابق كان هذا المعامل هو o) سيكون محليًّا وسيُنفّذ على نسخة من البيانات ولن يؤثر على النسخة الأصلية. سنرى فيما يلي أمثلة تستخدم مُستقبلات مرجعية "مؤشر". مستقبلات مثل مؤشرات يُشبه استخدام المؤشرات مثل مستقبلات في التوابع إلى حد كبير استخدام "مستقبل القيمة"، والفرق الوحيد هو وضع علامة * قبل اسم النوع. يوضح المثال التالي تعريف تابع على مستقبل مؤشر إلى نوع: package main import "fmt" type Boat struct { Name string occupants []string } func (b *Boat) AddOccupant(name string) *Boat { b.occupants = append(b.occupants, name) return b } func (b Boat) Manifest() { fmt.Println("The", b.Name, "has the following occupants:") for _, n := range b.occupants { fmt.Println("\t", n) } } func main() { b := &Boat{ Name: "S.S. DigitalOcean", } b.AddOccupant("Sammy the Shark") b.AddOccupant("Larry the Lobster") b.Manifest() } سيكون الخرج على النحو التالي: The S.S. DigitalOcean has the following occupants: Sammy the Shark Larry the Lobster يُعرّف هذا المثال نوعًا يُسمّى Boat يمتلك حقلين هما Name و occupants. نريد حماية الحقل occupants بحيث لا تتمكن الشيفرات الأخرى من خارج الحزمة أن تُعدّل عليه إلا من خلال التابع AddOccupant، لهذا السبب جعلناه حقلًا غير مُصدّر، وذلك بجعل أول حرف صغيرًا. نريد أيضًا جعل التعديلات التي يُجريها التابع AddOccupant على نفس المتغير وبالتالي نحتاج إلى تمريره بالمرجع؛ أي يجب أن نُعرّف مُستقبل مؤشر (b *Boat) وليس مُستقبل قيمة، إذ سيعمل مُستقبل المؤشر على إجراء التعديلات على نفس بيانات المتغير الأصلي الموجودة في الذاكرة من خلال تمرير عنوانه. تعمل المؤشرات مثل مراجع إلى متغير من نوع محدد بدلاً من نسخة من هذا النوع، لذلك سيضمن تمرير عنوان متغير من النوع Boat إلى التابع AddOccupant تنفيذ التعديلات على المتغير نفسه وليس نسخةً منه. نُعرّف داخل الدالة main متغيرًا b يحمل عنوان بنية من النوع Boat، وذلك من خلال وضع & قبل تعريف البنية (Boat*) كما في الشيفرة أعلاه. استدعينا التابع AddOccupant مرتين لإضافة عُنصرين. يُعرّف التابع Manifest على النوع Boat ويستخدم مُستقبل قيمة (b Boat). ما زلنا قادرين على استدعاء Manifest داخل الدالة main لأن لغة جو قادرة على تحصيل dereference قيمة المؤشر تلقائيًا من Boat، إذ تكافئ ()b.Manifest هنا ()b).Manifest*). بغض النظر عن نوع المستقبل الذي تُعرّفه لتابع ما؛ مستقبل مؤشر أو مستقبل القيمة، فإن له آثارًا مهمة عند محاولة إسناد قيم للمتغيرات من نوع واجهة. المستقبلات مثل مؤشرات والواجهات عندما تحاول إسناد قيمة متغير من نوع محدد إلى متغير نوعه واجهة، يتأكد مُصرّف جو ما إذا كان ذلك النوع يُنفّذ كل التوابع التي تتطلبها الواجهة. تختلف مجموعة التوابع لمستقبل المؤشر عن مستقبل القيمة لأن التوابع التي تتلقى مؤشرًا يمكنها التعديل على المُستقبل الخاص بها، بينما لا يمكن لتلك التي تتلقى قيمة فعل ذلك. package main import "fmt" type Submersible interface { Dive() } type Shark struct { Name string isUnderwater bool } func (s Shark) String() string { if s.isUnderwater { return fmt.Sprintf("%s is underwater", s.Name) } return fmt.Sprintf("%s is on the surface", s.Name) } func (s *Shark) Dive() { s.isUnderwater = true } func submerge(s Submersible) { s.Dive() } func main() { s := &Shark{ Name: "Sammy", } fmt.Println(s) submerge(s) fmt.Println(s) } سيكون الخرج على النحو التالي: Sammy is on the surface Sammy is underwater عرّفنا واجهة تُسمى Submersible، تقبل أنواعًا تُنفّذ التابع ()Dive الخاص بها. عرّفنا أيضًا النوع Shark مع الحقل Name والتابع isUnderwater لتتبع حالة متغيرات هذا النوع. عرّفنا أيضًا التابع ()Dive مع مُستقبل مؤشر من النوع Shark، إذ يُعدّل هذا التابع قيمة التابع isUnderwater لتصبح true. عرّفنا أيضًا التابع ()String مع مُستقبل قيمة من النوع Shark لطباعة حالة Shark بأسلوب مُرتب باستخدام fmt.Println ومن خلال الواجهة fmt.Stringer التي تعرّفنا عليها مؤخرًا. عرّفنا أيضًا الدالة submerge التي تأخذ معاملًا من النوع Submersible. يسمح لنا استخدام الواجهة Submersible بدلًا من Shark* في الدالة submerge جعل هذه الدالة تعتمد فقط على السلوك الذي يوفره النوع، وبالتالي جعلها أكثر قابلية لإعادة الاستخدام، فلن تضطر إلى كتابة دوال submerge جديدة لحالات خاصة أخرى مثل Submarine أو Whale أو أي كائنات مائية أخرى في وقت لاحق، فطالما أنها تُعرّف التابع ()Dive يمكنها أن تعمل مع الدالة submerge. نُعرّف داخل الدالة main المتغير s الذي يمثّل مؤشرًا إلى Shark ونطبعه مباشرةً باستخدام الدالة fmt.Println مما يؤدي إلى طباعة Sammy is on the surface على شاشة الخرج. نمرر بعدها المتغير s إلى الدالة submerge ثم نستدعي الدالة fmt.Println مرةً أخرى على المتغير s مما يؤدي لطباعة Sammy is underwater. إذا جعلنا s من النوع Shark بدلًا من Shark*، سيعطي المُصرّف الخطأ التالي: cannot use s (type Shark) as type Submersible in argument to submerge: Shark does not implement Submersible (Dive method has pointer receiver) يخبرنا مُصرّف جو أن Shark تمتلك التابع Dive وأن ذلك معرّفٌ في مستقبل المؤشر. عندما ترى رسالة الخطأ هذه، فإن الحل هو تمرير مؤشر إلى نوع الواجهة باستخدام العامل & قبل اسم المتغير الذي ترغب بإسناده لمتغير آخر. خاتمة لا يختلف التصريح عن توابع في لغة جو كثيرًا عن تعريف الدوال التي تستقبل أنواعًا مختلفة من المتغيرات، إذ تنطبق نفس قواعد العمل مع المؤشرات. توفر لغة جو الراحة في تعريف الدوال ويسمح بجمعها في مجموعات من التوابع التي يمكن تفسيرها من خلال أنواع الواجهة. سيسمح لك استخدام التوابع بطريقة فعّالة بالعمل مع الواجهات في شيفراتك البرمجية وتحسين إمكانية الاختبار، كما يجعل الشيفرة أكثر تنظيمًا وسهلة القراءة للمطورين الآخرين. ترجمة -وبتصرف- للمقال Defining Methods in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: البنى Structs في لغة جو Go التعامل مع السلاسل في لغة جو Go المؤشرات Pointers في لغة جو Go
  9. يُعد بناء تجريدات abstractions لتفاصيل البرنامج الخاص بك أعظم أداة يمكن أن تقدمها لغة البرمجة للمطور، إذ تسمح البُنى structs لمطوري لغة جو بوصف العالم الذي يعمل فيه البرنامج؛ فبدلًا من استخدام السلاسل النصية strings لوصف أشياء، مثل الشارع Street والمدينة City والرمز البريدي PostalCode، يمكن استخدام بنية تجمع كل هذه الأشياء تحت مفهوم "العنوان Address". يمكن تعريف البنية على أنها هيكل بيانات يُستخدم لتعريف نوع بيانات جديد يحتوي مجموعةً محددةً من القيم مختلفة النوع، ويمكن الوصول لهذه العناصر أو القيم عن طريق اسمها. تساعد البنى المطورين المستقبليين (بما في ذلك نحن) بتحديد البيانات المهمة للبرامج الخاصة بنا وكيف يجب أن تستخدم الشيفرات المستقبلية هذه البيانات بالطريقة الصحيحة. يمكن تعريف البنى واستخدامها بعدة طرق مختلفة. سنلقي نظرةً في هذا المقال على كل من هذه التقنيات. تعريف البنى يمكنك أن تتخيل البنى مثل نماذج جوجل التي يُطلب منك ملؤها أحيانًا. تتضمّن هذه النماذج حقولًا يُطلب منك تعبئتها، مثل الاسم، أو العنوان، أو البريد الإلكتروني، أو مربعات فارغة يمكن تحديد إحداها لوصف حالتك الزوجية (أعزب، متزوج، أرمل) …إلخ. يمكن أن تتضمن البُنى أيضًا حقولًا يمكنك تعبئتها. تهيئة متغير ببنية جديدة، أشبه باستخراج نسخة من نموذج جاهز للتعبئة. لإنشاء بنية جديدة يجب أولاً إعطاء لغة جو مُخططًا يصف الحقول التي تحتوي عليها البنية. يبدأ تعريف البنية بالكلمة المفتاحية type متبوعًا باسم البنية الذي تختاره Creature ثم الكلمة struct ثم قوسين {} نضع بداخلهما الحقول التي نريدها في البنية. يمكنك استخدام البنية بعد الانتهاء من التصريح عنها مع المتغيرات كما لو أنها نوع بيانات بحد ذاته. package main import "fmt" type Creature struct { Name string } func main() { c := Creature{ Name: "Sammy the Shark", } fmt.Println(c.Name) } ستحصل عند تشغيل البرنامج على الخرج التالي: Sammy the Shark صرّحنا في هذا المثال أولًا عن بنية اسمها Creature تتضمّن حقلًا اسمه Name من نوع سلسلة نصية string. نُعرّف داخل الدالة main مُتغيرًا c من النوع Creature ونهيئ الحقل Name فيه بالقيمة "Sammy the Shark"، إذ نفتح قوسين {} ونضع هذه المعلومات بينهما كما في الشيفرة أعلاه. أخيرًا استدعينا الدالة fmt.Println وطبعنا من خلالها الحقل Name من خلال المتغير c وذلك عن طريق وضع اسم المتغير متبوعًا بنقطة . ومتبوعًا باسم الحقل، مثل c.Name، الذي يعيد في هذه الحالة حقل Name. عندما نأخذ نسخةً من بنية، نذكر غالبًا اسم كل حقل ونسند له قيمة (كما فعلنا في المثال السابق). عمومًا، إذا كنت ستؤمن قيمة كل حقل أثناء تعريف نسخة من بنية فيمكنك عندها تجاهل كتابة أسماء الحقول، كما يلي: package main import "fmt" type Creature struct { Name string Type string } func main() { c := Creature{"Sammy", "Shark"} fmt.Println(c.Name, "the", c.Type) } وسيكون الخرج على النحو التالي: Sammy the Shark أضفنا حقلًا جديدًا للبنية Creature باسم Type وحددنا نوعه string. أنشأنا داخل الدالة main نسخةً من هذه البنية وهيّأنا قيم الحقلين Name و Type بالقيمتين "Sammy" و "shark" على التوالي من خلال التعليمة: Creature{"Sammy", "Shark"} واستغنينا عن كتابة أسماء الحقول صراحةً. نُسمي هذه الطريقة بالطريقة المُختصرة للتصريح عن نسخة من بنية، وهذه الطريقة غير شائعة كثيرًا لأنها تتضمن عيوبًا مثل ضرورة تحديد قيم لجميع الحقول وبالترتيب وعدم نسيان أي حقل. نستنتج سريعًا أن استخدام هذه الطريقة لا يكون جيدًا عندما يكون لدينا عددٌ كبيرٌ من الحقول لأننا سنكون عُرضةً للخطأ والنسيان والتشتت عندما نقرأ الشيفرة مرةً أخرى. إذًا، يُفضّل استخدام هذه الطريقة فقط عندما يكون عدد الحقول قليل. ربما لاحظت أننا نبدأ أسماء جميع الحقول بحرف كبير، وهذا مهم جدًا لأنه يلعب دورًا في تحديد إمكانية الوصول إلى هذه الحقول؛ فعندما نبدأ اسم الحقل بحرف كبير، فهذا يعني إمكانية الوصول إليه من خارج الحزمة، أما الحرف الصغير فلا يمكن الوصول إليها من خارج الحزمة. تصدير حقول البنية يعتمد تصدير حقول البنية على نفس قواعد تصدير المكونات الأخرى في لغة جو؛ فإذا بدأ اسم الحقل بحرف كبير، فسيكون قابلاً للقراءة والتعديل بواسطة شيفرة من خارج الحزمة التي صُرّح عنه فيها؛ أما إذا بدأ الحقل بحرف صغير، فلن تتمكن من قراءة وتعديل هذا الحقل إلا من شيفرة من داخل الحزمة التي صُرّح عنه فيها. يوضّح المثال التالي الأمر: package main import "fmt" type Creature struct { Name string Type string password string } func main() { c := Creature{ Name: "Sammy", Type: "Shark", password: "secret", } fmt.Println(c.Name, "the", c.Type) fmt.Println("Password is", c.password) } وسيكون الخرج على النحو التالي: Sammy the Shark Password is secret أضفنا إلى البنية السابقة حقلًا جديدًا secret، وهو حقل من النوع string ويبدأ بحرف صغير؛ أي أنه غير مُصدّر، وأي حزمة أخرى تحاول إنشاء نسخة من هذه البنية Creature لن تتمكن من الوصول إلى حقل secret. عمومًا، يمكننا الوصول إلى هذا الحقل ضمن نطاق الحزمة، لذا إذا حاولنا الوصول إلى هذا الحقل من داخل الدالة main والتي بدورها موجودة ضمن نفس الحزمة بالتأكيد، فيمكننا الرجوع لهذا الحقل c.password والحصول على القيمة المُخزنة فيه. وجود حقول غير مُصدّرة أمر شائع في البنى مع إمكانية وصول بواسطة توابع مُصدّرة exported. البنى المضمنة Inline تُسمى أيضًا البنى السريعة. تُمكّنك لغة جو من تعريف بُنى في أي وقت تريده وفي أي مكان ودون الحاجة للتصريح عنها على أنها نوع بيانات جديد بحد ذاته، وهذا مفيد في الحالات التي تكون فيها بحاجة إلى استخدام بنية مرةً واحدةً فقط (أي لن تحتاج إلى إنشاء أكثر من نسخة)، فمثلًا، تستخدم الاختبارات غالبًا بنيةً لتعريف جميع المعاملات التي تُشكل حالة اختبار معينة. سيكون ابتكار أسماء جديدة مثل CreatureNamePrintingTestCase مرهقًا لدى استخدام هذه البنية في مكان واحد فقط. يكون كتابة البنى المُضمّنة بنفس طريقة كتابة البنى العادية تقريبًا، إذ نكتب الكلمة المفتاحية struct متبوعةً بقوسين {} نضع بينهما الحقول وعلى يمين اسم المتغير. يجب أيضًا وضع قيم لهذه الحقول مباشرةً من خلال استخدام قوسين آخرين {} كما هو موضح في الشيفرة التالية: package main import "fmt" func main() { c := struct { Name string Type string }{ Name: "Sammy", Type: "Shark", } fmt.Println(c.Name, "the", c.Type) } ويكون الخرج كما يلي: Sammy the Shark لاحظ هنا أننا لم نحتاج إلى تعريف نوع بيانات جديد لتمثيل البنية، وبالتالي لم نكتب الكلمة المفتاحية type، فكل ما نحتاجه هنا هو استخدام الكلمة المفتاحية struct إشارةً إلى البنية، وإلى معامل الإسناد القصير =:. نحتاج أيضًا إلى تعريف قيم الحقول مباشرةً كما فعلنا في الشيفرة أعلاه. الآن أصبح لدينا متغيرًا اسمه c يُمثّل بنيةً يمكن الوصول لحقولها من خلال النقطة . كما هو معتاد. سترى البنى المُضمّنة غالبًا في الاختبارات، إذ تُعرّف البنى الفردية بصورةٍ متكررة لاحتواء البيانات والتوقعات لحالة اختبار معينة. خاتمة البُنى هي كتل بيانات غير متجانسة (أي أنها تضم حقولًا أو عناصر من أنواع بيانات مختلفة) يعرّفها المبرمجون لتنظيم المعلومات. تتعامل معظم البرامج مع أحجام هائلة من البيانات، وبدون البنى سيكون من الصعب تذكر أي من المتغيرات ترتبط معًا وأيّها غير مرتبطة أو أيّها من نوع string وأيها من نوع int. لذلك إذا كنت تتعامل مع مجموعة من المتغيرات، اسأل نفسك عما إذا كان تجميعها ضمن بنية سيكون أفضل، إذ من الممكن أن تصف هذه المتغيرات مفهومًا عالي المستوى، فيمكن مثلًا أن يشير أحد المتغيرات إلى عنوان شركة حسوب وهناك متغيّر آخر يخص عنوان شركة أُخرى. ترجمة -وبتصرف- للمقال Defining Structs in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: المؤشرات Pointers في لغة جو Go هياكل البيانات المكدس Stack والرتل Queue وأنواع البيانات المجردة ADT
  10. تعلم الآلة machine learning هو أحد الدعائم الأساسية لتكنولوجيا المعلومات على مدار العقدين الماضيين، وبات اليوم جزءًا أساسيًا من حياتنا اليومية. فمع توفر كميات متزايدة من البيانات أصبح هناك سببٌ وجيهٌ للاعتقاد بأن التحليل الذكي للبيانات سيكون أكثر انتشارًا وأهميةً كمكون أساسي للتقدم التكنولوجي. في المقالة السابقة قدمنا دليلًا شاملًا لبدء تعلم الذكاء الاصطناعي وفي هذه المقالة والتي تعتبر جزءًا من سلسلة مقالات عن الذكاء الصناعي بدأناها مع مقال مقدمة شاملة عن الذكاء الاصطناعي نُضيء لك الطريق ونقدم لك دليلًا شاملًا للبدء بتعلم وفهم تعلم الآلة والتعرف على أهم المصطلحات المرتبطة بهذا التخصص. ملاحظة: إن لم يكن لديك معرفة مسبقة بمجال الذكاء الصناعي وفروعه وأساسياته، سيكون من الأفضل أن تقرأ مقال "مقدمة شاملة عن الذكاء" من هذه السلسلة على الأقل التي أشرت إليها للتو. ما المقصود بتعلم الآلة؟ تعلم الآلة Machine learning أو ما يعرف اختصارًا ML هو مصطلح تقني يعني استخدام مجموعة من التقنيات والأدوات التي تساعد أجهزة الحاسوب والآلات الذكية عمومًا على التعلم والتكيف من تلقاء نفسها. لقد نشأ علم التعلم الآلي عندما بدأ علماء الحاسوب بطرح الأسئلة التالية: نحن البشر نتعلم من التجارب السابقة أما الآلات فهي تفعل ما نمليه عليها فقط فهل يمكن أن نتمكن من تدريب هذه الآلات كي تتعلم من البيانات والخبرات السابقة وتحاكي طريقة تفكيرنا وتتمكن من التعلم والفهم والاستنتاج دون تدخلنا؟ هل يمكن للحواسيب أن تفعل ما نفعله وبالطريقة التي نريدها، وأن تتعلم من تلقاء نفسه كيفية أداء مهمة محددة؟ هل يمكن للحواسيب والآلات أن تفاجئنا وتتعلم من خلال البيانات من تلقاء نفسه بدلًا من قيام المبرمجين بصياغة قواعد معالجة البيانات لها بشكل يدوي؟ كل هذه التساؤلات فتحت الباب أمام نموذج برمجة بديل عن أسلوب البرمجة الكلاسيكية التي يُدخل فيها البشر القواعد ضمن برامج حاسوبية ويحددون بدقة البيانات التي يجب معالجتها وفقًا لهذه القواعد ويكون الخرج إجابات محددة ناتجة عن عمليات المعالجة. في نموذج البرمجة الجديد هذا الذي سمي تعلم الآلة أصبح بمقدور البشر إدخال البيانات للحاسوب بالإضافة إلى الإجابات المتوقعة وفقًا للبيانات، ويكون الخرج هو القواعد التي تم استنتاجها بشكل برنامج أو ما يُسمى بالنموذج model ثم يمكن بعد ذلك تطبيق هذه القواعد على البيانات الجديدة لإنتاج الإجابات. بمعنى آخر نظام التعلم الآلي يُدرّب بدلًا من برمجته صراحةً، حيث تقدّم له العديد من عينات البيانات المتعلقة بالمهمة المطلوبة، ليكتشف بنية إحصائية في هذه العينات تربط المدخلات بالمخرجات (البيانات بالإجابات) وتسمح في النهاية للنظام بالوصول إلى قواعد لأتمتة المهمة. على سبيل المثال إذا عرضنا على الحاسوب مجموعات ثنائية من الأرقام كالتالي (2 , 4) و (3 , 6) و (4 , 9) ثم طلبنا منه بناء على هذه المعطيات أن يتنبأ بالرقم الذي يجب أن يأتي مع الرقم 5 فسوف يحتاج في بداية الأمر إلى إيجاد المنطق بين مجموعات البيانات التي مررناها له وتطبيق نفس المنطق للتنبؤ بالرقم الجديد، عملية العثور على هذا المنطق يسمى تعلم الآلة وبعد إيجاد هذا المنطق سنتمكن من تطبيقه للتنبؤ بكل رقم جديد. و إذا كنت ترغب بأتمتة عملية تخمين أسعار المنازل -يمكنك إعطاء نظام التعلم الآلي مجموعة من العينات التي تمثّل مواصفات المنازل (بيانات) مع أسعارها (إجابات)، وسيتعلم النظام القواعد الإحصائية اللازمة لربط المواصفات بالأسعار. على الرغم من أن التعلم الآلي بدأ في الازدهار بدءًا من التسعينيات، إلا أنه سرعان ما أصبح المجال الفرعي الأكثر شعبية والأكثر نجاحًا في الذكاء الصناعي. تعريف تعلم الآلة؟ هناك العديد من التعاريف الخاصة بتعلم الآلة نذكر منها أهم تعريفين: تعريف آرثر صامويل 1959: "تعلم الآلة هو المجال الدراسي الذي يمنح أجهزة الحاسب القدرة على التعلم دون برمجتها صراحةً من قبل البشر" تعريف توم ميتشل: "تعلم الآلة هو دراسة خوارزميات الحاسوب التي تحسن أدائها تلقائيًا من خلال التجربة ويقال أَنَّ برنامجا حاسوبيًا يتعلَّم من الخبرة E التي تخص بمجموعة من المهام T بالنسبة إِلى مقياس الأداء P إذا تحسن أداءه على إنجاز المهام T بعد اكتساب الخبرة E بالمقدار P" على سبيل المثال في مجال لعب الشطرنج تكون الخبرة E هي عدد مرات اللعب ضد الحاسب و T هي مهمة لعب الشطرنج ضد الحاسب والمعيار P هو فوز/خسارة الحاسب. ويمكننا القول بتعريف بسيط أن تعلم الآلة مجال من مجالات الذكاء الصناعي يحاول بناء آلات قادرة على التعلّم من تلقاء نفسها. الفرق بين الذكاء الاصطناعي وتعلم الآلة يميل معظم الناس إلى استخدام مصطلحي الذكاء الاصطناعي وتعلم الآلة كمرادفين ولا يعرفون الفرق، إلا أن هذين المصطلحين هما في الواقع مفهومان مختلفان، فتعلم الآلة هو في الواقع جزء من الذكاء الاصطناعي أو أحد أنواع الذكاء الاصطناعي ويمكن القول أن الذكاء الاصطناعي هو مجال واسع من الموضوعات، ويشكل تعلم الآلة جزء صغير من هذه الموضوعات. يشير الذكاء الاصطناعي إلى القدرة العامة لأجهزة الحاسب على محاكاة الفكر البشري وأداء المهام في بيئات العالم الحقيقي، بينما يشير التعلم الآلي إلى التقنيات والخوارزميات التي تمكّن الأنظمة من تحديد طريقة لتعلّم الأنماط المخفيّة في البيانات واتخاذ القرارات وتحسين نفسها من خلال التجربة والبيانات. أهمية تعلم الآلة قد تتساءل عن أهمية مجال تعلم الآلة في عصرنا الحالي وللإجابة على هذا السؤال يمكننا تقسيم الإجابة إلى شقين الأول أهمية تعلم الآلة بالنسبة للمؤسسات وتطور الحياة عمومًا، والثاني أهميته بالنسبة لبقية فروع الذكاء الصناعي و الفروع ذات الصلة. بالنسبة للشق الأول، فالبيانات هي شريان الحياة لجميع قطاعات الأعمال وقد باتت القرارات التي تعتمد على البيانات تصنع الفارق وتشكل الفصل بين مواكبة المنافسة أو التخلف عن الركب. التعلم الآلي هو مفتاح إطلاق العنان لقيمة بيانات الشركات والعملاء واتخاذ القرارات التي تجعل الشركة في صدارة المنافسة. إذًا يعتبر التعلم الآلي مهمًا لأنه يمنح المؤسسات بمختلف أنواعها وجهة نظر حول اتجاهات وسلوك العملاء وأنماط تشغيل الأعمال فضلًا عن دعم عملية تطوير منتجات جديدة كما أنها تساعد في أمور أخرى مثل اكتشاف الأمراض وتوقعات الطقس …إلخ. وتستخدم العديد من الشركات الرائدة اليوم مثل فيسبوك Facebook وجوجل Google وأوبر Uber التعلم الآلي كجزء أساسي من عملياتها. بالنسبة للشق الثاني، ففروع الذكاء الصناعي الأخرى مثل معالجة اللغات الطبيعية أو الفروع ذات الصلة كالرؤية الحاسوبية لم تبصر النور إلا بعد التطورات الأخيرة التي شهدها تعلم الآلة الذي شكَّل ركيزة أساسية في تطور معظم فروع الذكاء الصناعي. على سبيل المثال تعد تطبيقات التعرف على الكائنات أو تتبعها أو تصنيف الأشياء قضايا شائعة في الرؤية الحاسوبية، وهي تعتمد بشكل شبه مطلق على خوارزميات التعلم العميق والأمر ذاته ينطبق على معالجة اللغات الطبيعية. البيانات وتعلم الآلة البيانات هي وقود التعلم الآلي، فبدون البيانات ستكون خوارزميات التعلم الآلي جائعة وضعيفة الأداء وستفشل في حل أي مشكلة بشكل صحيح. وبعض خوارزميات التعلم الآلي مثل خوارزميات التعلم العميق لا تشبع، وكلما زودتها بالبيانات ونوّعتها لها كلما ازدادت قوتها وقدراتها وتحسن أداؤها. وتجدر الإشارة هنا لأن الأمر لا يتعلق بزيادة حجم البيانات وكميتها فقط بل يتعلق كذلك بدقة البيانات وجودتها وتنوع حالات استخدامها للحصول على نتائج صحيحة ودقيقة. وفيما يلي نوضح أهم النقاط الأساسية المتعلقة بالبيانات والمرتبطة بالتعلم الآلي: هناك نوعان أساسيان للبيانات المستخدمة في التعلم الآلي: بيانات مُسماة labeled وغير مسماة unlabeled على سبيل المثال إذا كان المطلوب هو التنبؤ بعمر شخص ما فإن العمر هنا يعد من البيانات المسماة في حين أن البيانات غير المسماة لا تحتوي على أي سمة مميزة. تسمى خوارزميات تعلم الآلة التي تتعامل مع مجموعة بيانات مسماة خوارزميات التعلم الخاضع للإشراف في حين تسمى خوارزميات تعلم الآلة التي تستخدم مجموعة بيانات غير مسماة خوارزميات التعلم غير الخاضع للإشراف وسنشرح المزيد عن هذه الأنواع من التعلم الآلي في فقرة لاحقة. عادةً ما تكون البيانات المستخدمة في التعلم الآلي عددية Numerical أو فئوية Categorical. تتضمن البيانات العددية القيم التي يمكن ترتيبها وقياسها مثل العمر أو الدخل، وتتضمن البيانات الفئوية القيم التي تمثل الفئات، مثل الجنس أو نوع الفاكهة. يمكن تقسيم البيانات إلى مجموعات تدريب Training ومجموعات مراقبة أو تحقق Validation ومجموعات اختبار Testing حيث تُستخدم مجموعة بيانات التدريب لتدريب النموذج، وتُستخدم مجموعة المراقبة لمراقبة سير عملية التدريب وتعلّم النموذج وأخيرًا تُستخدم مجموعة الاختبار للتقييم النهائي لأداء النموذج. تعد المعالجة المسبقة للبيانات خطوة مهمة في تعلم الآلة حيث يتم من خلالها تنظيف البيانات ومعالجة القيم المفقودة والشاذة وهندسة الميزات. هناك فرق بين البيانات والمعلومات والمعرفة، فالبيانات يمكن أن تكون أي حقيقة أو قيمة أو نص أو صوت أو صورة غير معالجة لم تُفسّر أو تحلل أما المعلومات فهي البيانات بعد تفسيرها ومعالجتها. أي أنها شكل متطور للبيانات ذو فائدة ومعنى أكبر والمعرفة هي المرحلة الأكثر تطورًا والمتمثلة بالوعي والفهم والإدراك للمعلومات. تطبيقات تعلم الآلة للتعلم الآلي تطبيقات في جميع أنواع الصناعات وفي حياتنا اليومية عمومًا، يتضمن ذلك التصنيع والتجارة بالتجزئة والرعاية الصحية وعلوم الحياة والسفر والضيافة والخدمات المالية والطاقة والمواد الأولية والمرافق. بما أن تعلم الآلة يندرج تحت مظلة الذكاء الاصطناعي، فإن تطبيقات تعلم الآلة تدخل ضمن أي تطبيق ذكاء اصطناعي وقد تحدثنا عن تطبيقات الذكاء الاصطناعي في مقال تطبيقات الذكاء الاصطناعي لذا يمكنك الرجوع إليه. تشمل بعض حالات الاستخدام لمجال تعلم الآلة: التعرّف على الصور التعرف على الكلام التصنيف والتنبؤ الآلات والروبوتات الذكية -السيارات ذاتية القيادة التجارة الإلكترونية الأمان واكتشاف التزوير أو الاحتيال -الترجمة الآلية والخدمات المالية …إلخ. يُستخدم التعلم الآلي في العديد من المجالات الأخرى والتي قد لا يسعنا ذكر جميعها هنا. كما أن معظم تطبيقات الذكاء الصناعي اليوم تعتمد بشكل كامل على خوارزميات تعلم الآلة. وفي الفيديو التالي ستتعرف على المزيد من المعلومات حول معنى تعلم الآلة وأهدافها والمكونات الرئيسية لأي نظام معتمد على تعلم الآلة وأمثلة منوعة على التطبيقات المستخدمة في هذا المجال. أنواع تعلم الآلة ينقسم تعلم الآلة إلى أربعة أنواع أساسية أو مجالات أساسية بناءً على نوع البيانات المستخدمة وأسلوب التعلم المتبع وهذه الأنواع هي: التعلم الخاضع للإشراف Supervised learning التعلم غير الخاضع للإشراف UnSupervised learning التعلم شبه الخاضع للإشراف Semi-Supervised Learning التعلم المُعزز Reinforcement Learning يحاكي كل نوع من هذه الأنواع الطرق والأساليب المختلفة التي تتخذ بها نحن البشر قرارتنا وإليك شرحًا مفصلًا لكل نوع من بين هذه الأنواع. 1. التعلم الخاضع للإشراف Supervised learning هو أحد أكثر أنواع التعلم الآلي شيوعًا ونجاحًا. في هذا النوع تُعطى الخوارزمية مجموعةً من البيانات بالإضافة إلى الخرج الصحيح لها. أي أن الخوارزمية تُدرّب على مجموعةً بيانات مُسماة. مبدأ عمل الخوارزميات التي تنتهج هذا النهج هو: الحصول على مجموعة بيانات مُسماة، ثم تدريب النموذج على إيجاد علاقة أو دالة function تربط بين المدخلات والمخرجات. إذًا يمكننا القول أن هذا النهج يُستخدم عندما نريد بناء نموذج يتنبأ (كلمة يتنبأ هنا تشمل التصنيف أيضًا) بنتيجة معينة في ضوء مدخلات معينة ويتطلب تعلم الآلة الخاضع للإشراف جهدًا بشريًا لبناء مجموعة التدريب المناسبة. هناك نوعان رئيسيان من مشكلات التعلم الآلي الخاضعة للإشراف هما التصنيف Classification والتوقع Regression. يكون الهدف في حالة التصنيف هو التنبؤ بتسمية فئة من قائمة محددة مسبقًا من الفئات. المثال الذي ورد في الفقرة السابقة هو خير مثال على ذلك، حيث نُدرّب الخوارزمية على التمييز بين فئتين أو نوعين من الفاكهة هما البرتقال والتفاح. التصنيف بدوره يُقسم إلى نوعين أساسيين هما: التصنيف الثنائي Binary Classification والتصنيف المتعدد Multiclass-Classification. يحدث التصنيف الثنائي عندما يكون لدينا فئتين من البيانات كما في مثال البرتقال والتفاح، مثال آخر هو تصنيف رسائل البريد الإلكتروني على أنها بريد عشوائي أو بريد حقيقي أما التصنيف المتعدد فهو يحدث عندما يكون لدينا أكثر من فئتين (مثلًا لو كان المطلوب جعل النموذج سالف الذكر يميز بين 3 أنواع من الفاكهة هي البرتقال والتفاح والموز. بالنسبة لمهام التوقع، فإن الهدف يكون توقع قيمة من مجال غير محدد وخير مثال على ذلك هو توقع أسعار المنازل أو توقع الدخل السنوي للفرد بناءًا على مستواه التعليمي وعمره ومكان إقامته فسعر المنازل أو الدخل السنوي يمكن أن تكون أي قيمة ضمن مجال ما. 2. التعلم غير الخاضع للإشراف UnSupervised learning في هذا النوع من أنواع تعلم الآلة تُعطى الخوارزمية مجموعةً من البيانات بدون الخرج الصحيح لها، أي أن الخوارزمية تُدرّب على مجموعةً بيانات غير مُسماة. مبدأ عمل الخوارزميات التي تنتهج هذا النهج هو: الحصول على مجموعة بيانات غير مُسماة، ثم تدريب النموذج على إيجاد علاقات مخفيّة بين هذه المُدخلات. هناك نوعان أساسيين من الخوارزميات التي تنتهج هذا النهج هما خوارزميات التحويل Transformation وخوارزميات العنقدة أو التجميع Clustering. تُنشئ خوارزميات التحويل تمثيلًا جديدًا للبيانات قد يكون من الأسهل على البشر أو خوارزميات التعلم الآلي الأخرى فهمه مقارنةً بالتمثيل الأصلي للبيانات. بالنسبة للنوع الآخر المتمثل بخوارزميات العنقدة، فهي تعتمد على تقسيم البيانات إلى مجموعات مميزة من العناصر المتشابهة. خذ مثلًا عملية تحميل الصور على أحد مواقع التواصل الاجتماعي كمثال، هنا قد يرغب الموقع في تجميع الصور التي تُظهر الشخص نفسه مع بعضها بغية تنظيم صورك إلا أن الموقع لا يعرف من يظهر في الصور ولا يعرف عدد الأشخاص المختلفين الذين يظهرون في مجموعة الصور خاصتك. تتمثل الطريقة المنطقية لحل المشكلة في استخراج كل الوجوه وتقسيمها إلى مجموعات من الوجوه المتشابهة، وبعدها يتم وضع كل صورة جديدة تحملها في المجموعة الأكثر شبهًا لها ومن أشهر خوارزميات العنقدة نذكر خوارزمية K-means. التعلم شبه الخاضع للإشراف Semi-Supervised Learning تحدثنا سابقًا عن أهمية البيانات، وأنّه كلما زاد حجم البيانات التي نُدرّب النموذج عليها تزداد قوته، ولاسيما بالنسبة خوارزميات التعلم العميق التي تنتهج النهج الخاضع للإشراف والتي تعتمد على البيانات المُسماة. نشأ التعلم شبه الخاضع للإشراف من السؤال التالي: ماذا لو لم يكن لدينا ما يكفي من البيانات المُسماة لخوارزميتنا التي تنتهج المنهج الخاضع للإشراف؟ قد يخطر ببالك في هذه الحالة محاولة تسمية البيانات يدويًا لكن هذا ليس مضيعة للوقت فقط لكنه أيضًا عرضة للعديد من أنواع الأخطاء البشرية الحل البديل الجديد هو استخدام التعلّم شبه الخاضع للإشراف وهو مزيج من التعلم الخاضع للإشراف والتعلم غير الخاضع للإشراف فهو يستخدم البيانات غير المسماة (والتي تكون كثيرة) والبيانات المسماة (والتي تكون قليلة) معًا، لإنشاء تقنيات تعلم أفضل من تلك التي تُنشأ باستخدام البيانات غير المسماة فقط أو المسماة فقط. التعلم المعزز Reinforcement Learning في هذا النوع من تعلم الآلة ابتكر المطورون طريقة لمكافأة السلوكيات المرغوبة ومعاقبة السلوكيات السلبية للآلة حيث يعتمد التعلم المُعزز على التعلم من خلال التفاعل مع البيئة أي أن الخوارزمية أو الروبوت أو البرنامج والذي يسمى في هذه الحالة الوكيل "agent" يتعلم من من عواقب أفعاله بدلًا من تعليمه صراحةً. فالوكيل يختار أفعاله وقرارته المستقبلية على أساس تجاربه السابقة (الاستغلال) وأيضًا من خلال الخيارات الجديدة (الاستكشاف) أي أن التعلم يرتكز على مبدأ التجربة والخطأ ويتعلق الأمر باتخاذ الإجراءات المناسبة لزيادة المكافأة إلى أقصى حد في موقف معين. يتطلب التعلم المعزز عددًا كبيرًا من التفاعلات بين الوكيل والبيئة لجمع البيانات للتدريب ويستخدم في مجال الألعاب حيث تعد الألعاب من أبرز مجالات استخدام التعلم المعزز فهو قادر على تحقيق أداء خارق في العديد من الألعاب مثل لعبة Pac-Man. ومن الأمثلة على استخدامه هو تدريب السيارات على التوقف باستخدام نظام القيادة الآلي حيث يتم تعليم حاسوب السيارة أو الوكيل على الوقوف في المكان الصحيح من خلال قراءة بيانات أجهزة استشعار مثل الكاميرات ونظام تحديد المواقع GPS واتخاذ الإجراءات المناسبة مثل الضغط على الفرامل وتغيير الاتجاه ويحتاج الوكيل بالطبع إلى إيقاف السيارة مرارًا وتكرارًا باستخدام التجربة والخطأ ويتم تقديم إشارة مكافأة لتقييم جودة التجربة وتعزيز عملية التعلم. نماذج تعلم الآلة لقد وصل التعلم العميق وهو نوع متقدم من أنواع تعلم الآلة إلى مستوى من الاهتمام العام والاستثمار الصناعي لم يسبق له مثيل في تاريخ مجال علوم الحاسوب ولكنه ليس أول شكل أو نموذج ناجح للتعلم الآلي ويمكننا القول إن معظم خوارزميات التعلم الآلي المستخدمة في الصناعة هذه الأيام -ليست خوارزميات التعلم العميق. يوفر مجال تعلم الآلة العديد من النماذج machine learning models أشهرها التعلم العميق لكن في الواقع لا يعد التعلم العميق دائمًا الأداة المناسبة، ففي بعض الأحيان لا توجد بيانات كافية ليكون التعلم العميق قابلاً للتطبيق، ويفضل أن تُحلّ المشكلة بطريقة أفضل من خلال خوارزمية مختلفة وفيما يلي نوضح أهم أنواع طرق تعلم الآلة: النمذجة الاحتمالية Probabilistic modeling أساليب النواة Kernel Methods أشجار القرار Decision Trees والغابات العشوائية Random Forests الشبكات العصبية Neural Networks التعلم العميق Deep Learning لنتعرف على قرب على كل نموذج من هذه النماذج وأشهر استخداماته وخوارزمياته. النمذجة الاحتمالية Probabilistic modeling تعتمد النمذجة الاحتمالية على تطبيق مبادئ الإحصاء في تحليل البيانات. لقد كانت أحد أقدم أشكال التعلم الآلي، والتي لا تزال مستخدمة حتى يومنا هذا واحدة من أشهر الخوارزميات في هذه الفئة هي خوارزمية بايز الساذج Naive Bayes. أساليب النواة Kernel Methods عندما بدأت الشبكات العصبية تكتسب شهرة بين الباحثين في التسعينيات ظهر نهج جديد للتعلم الآلي سمي أساليب النواة وسرعان ما أدى لتراجع شعبية الشبكات العصبية، وأساليب النواة هي مجموعة من خوارزميات التصنيف، وأشهرها خوارزمية الآلة متجه الدعم (SVM). أشجار القرار Decision Trees والغابات العشوائية Random Forests هي إحدى طرق النمذجة التنبؤية التي تُستخدم في الإحصاء واستخراج البيانات وتعلم الآلة وهي عبارة عن هياكل شبيهة بالمخططات التدفقية تتيح لك تصنيف نقاط البيانات المُدخلة أو التنبؤ بقيم المخرجات بناءًا على البيانات المُدخلة مثلًا تعطيها مواصفات المنزل، فتتوقع السعر وقد تم تفضيلها على أساليب النواة. الشبكات العصبية Neural Networks حتى عام 2010 تقريبًا، كانت الشبكات العصبية مُهمّشة إلى أن بدأ عدد من الباحثين الذين كانوا ما يزالون يؤمنون بالشبكات العصبية في تحقيق نتائج مهمة باستخدامها، وبذلك بدأت التطورات على الشبكات العصبية تتسارع وتفرض هيمنتها في معظم مهام تعلم الآلة. التعلم العميق Deep Learning السبب الرئيسي وراء انتشار التعلم العميق وتطوره بسرعه هو أنه قدّم أداءً أفضل في العديد من المشكلات واختصر العديد من الخطوات المعقدة التي كانت موجودة في الأساليب السابقة، والتعلم العميق اليوم هو الأسلوب الأنجح في جميع المهام الحسية أو الإدراكية مثل مهام الرؤية الحاسوبية ومعالجة اللغات الطبيعية NLP. وللتعرف على المزيد من الأساليب المتطورة لخوارزميات وأدوات التعلم الآلي يمكنك إلقاء نظرة على مسابقات التعلم الآلي على موقع Kaggle نظرًا لبيئتها شديدة التنافس (بعض المسابقات لديها آلاف المشاركين وجوائز بملايين الدولارات) وللتنوع الكبير في مشكلات التعلم الآلي التي تغطيها. لغة بايثون وتعلم الآلة تعد لغة البرمجة بايثون اللغة الأكثر شهرة في مجال تعلم الآلة لعدد كبير من الأسباب فهي لغة سهلة التعلم والفهم و ولها شعبية كبيرة وتستخدم من قبل عدد كبير من الأشخاص والمؤسسات وتملك العديد من المكتبات وأطر العمل التي تختص في مجال الذكاء الصنعي وتعلم الآلة فيما يلي سنكتب كود تعلم آلة في بايثون، حيث سنستخدم خوارزمية أشجار القرار لتوقع نوع الفاكهة (برتقال أو تفاح) بناءًا على وزنها ونسيجها. from sklearn import tree # الخطوة 1 # الخطوة 2 X = [[140, 1], [130, 1], [150, 0], [170, 0]] y = [0, 0, 1, 1] # 0: apple, 1: orange # الخطوة 3 clf = tree.DecisionTreeClassifier() clf = clf.fit(X, y) # الخطوة 4 prediction = clf.predict([[160, 0]]) print(prediction) # النتيجة: 1 شرح تنفيذ الشيفرة: الخطوة الأولى: هي استيراد المكتبة ساي كيت ليرن sklearn التي تحتوي العديد من خوارزميات تعلم الآلة الجاهزة. الخطوة الثانية: هي تحديد بيانات التدريب. هنا سيكون لدينا 4 عينات، كل عينة تُمثّل نقطة بيانات الخطوة الثالثة: هي تدريب خوارزمية أشجار القرار على البيانات السابقة الهدف هنا هو جعل الخوارزمية قادرة على التنبؤ بنوع الفاكهة بعد أن ندربها على البيانات السابقة. الخطوة الرابعة: بعد الخطوة 3 نكون انتهينا من تدريب النموذج، أي أنه أصبح جاهزًا للاستخدام الفعلي. لذا نعطيه عينة بيانات، ونطلب منه توقع نوع الفاكهة. إذا كنت مهتمًا بإتقان تعلم الآلة باستخدام لغة البرمجة بايثون يمكنك البدء مع دورة الذكاء الاصطناعي AI التي توفرها أكاديمية حسوب والتي تبدأ معك من الصفر ولا تتطلب منك أي خبرة مسبقة فهي تبدأ معك بتعلم كل ما تحتاجه بلغة بايثون وتطوير تطبيقات تستفيد من دمج مختلف نماذج LLMs الحديثة مثل GPT من OpenAI و LLaMA 2 من Meta لتطوير تطبيقات ذكية بسهولة وكفاءة كما تشرح لك تحليل البيانات Data Analysis وتمثيلها مرئيًا، ومفاهيم تعلم الآلة Machine Learning والتعلم العميق Deep Learning وغيرها من خلال التركيز على الشرح العملي المعد من قبل مجموعة من المطورين المُختصين، الذين يُرافقونك طيلة الدورة وحتى بعد الدورة للإجابة على أي سؤال أو مشكلة تواجهك كما تضمن لك الدورة دخول السوق والحصول على فرصة عمل بعد تخرجك من الدورة خلال ستة أشهر. دورة الذكاء الاصطناعي احترف برمجة الذكاء الاصطناعي AI وتحليل البيانات وتعلم كافة المعلومات التي تحتاجها لبناء نماذج ذكاء اصطناعي متخصصة. اشترك الآن مصادر تعليمية لتعلم الآلة بالإضافة إلى دورة بايثون الشاملة التي ذكرناها للتو والتي تعد من أفضل الدورات العربية لتعلم بايثون وتعلم الآلة، توفر توفر لك أكاديمية حسوب أيضًا مجموعة من المقالات والدروس التعليمية ومنشورات الكتب العربية التي تعد من أفضل مصادر تعلم الآلة وتعلم البرمجة وعلوم الحاسوب نذكر لك منها: كتاب البرمجة بلغة بايثون كتاب مدخل إلى الذكاء الاصطناعي وتعلم الآلة كتاب عشرة مشاريع عملية عن الذكاء الاصطناعي سلسلة برمجة الذكاء الاصطناعي المفاهيم الأساسية لتعلم الآلة نظرة سريعة على مجال تعلم الآلة أدوات برمجة نماذج تعلم الآلة وننصحك بتفصّح ومتابعة قسم مقالات الذكاء الاصطناعي في أكاديمية لقراءة المقالات الجديدة المنشورة حول الذكاء الاصطناعي وتعلم الآلة وما يتعلق بهما. أفضل مكتبات تعلم الآلة في بايثون إليك قائمة بأهم المكتبات التي قد تحتاجها في تعلّم الآلة (جميعها تعمل في لغة بايثون): نمباي Numpy: من أفضل مكتبات بايثون في مجال تعلم الآلة والذكاء الصناعي فهي مكتبة قوية وشائعة الاستخدام تسهل معالجة المصفوفات والتعامل معها وتسهل عمليات معالجة البيانات والعمليات الإحصائية عليها. سي باي Scipy: توفر هذه المكتبة مجموعة من الحزم المخصصة للاستخدام في مجال للحوسبة العلمية والهندسة والتقنية وتحتوي على وحدات مختلفة للجبر الخطي والتكامل والإحصاءات. ساي كيت ليرن Scikit-learn: اختصارًا Sklearn وهي مكتبة فعالة توفر العديد من أدوات وخوارزميات تعلم الآلة. باندا Pandas: هي مكتبة تستخدم لمعالجة البيانات وتحليلها وتسهل عمليات تنظيم البيانات وفهرستها ووضعها ضمن جداول وإجراء العمليات عليها. ماتبلوتليب Matplotlib: هي مكتبة بايثون خاصة بتصميم الرسوميات والمخططات البيانية وإنشاء رسومات تفاعلية وتصديرها إلى تنسيقات مختلفة. إضافة للعديد من أطر العمل القوية مثل ثيانو Theano وتنسرفلو TensorFlow وكيراس Keras وباي تورش PyTorch التي تسهل بناء نماذج الشبكات العصبية وتطبيق خوارزميات التعلم العميق. مستقبل تعلم الآلة بالرغم من أن خوارزميات تعلم الآلة كانت موجودة منذ عقود إلا أنها اكتسبت شعبية أكبر مع تنامي الذكاء الاصطناعي فمعظم تطبيقات الذكاء الصناعي المتقدمة التي نراها اليوم هي تطبيقات تعمل باستخدام خوارزميات تعلم الآلة (لاسيما التعلم العميق) بدءًا من خوارزميات التوصية التي نراها في موجز أخبار فيسبوك وغيرها من شبكات التواصل الاجتماعي والتي تعتمد على اهتماماتنا وعاداتنا في القراءة وصولًا السيارات ذاتية القيادة مثل سيارات تسلا Tsla ومعالجة اللغات الطبيعية مثل تطبيق الدردشة ChatGPT. كما تندمج الحلول التي يقدمها تعلم الآلة يومًا بعد يوم في العمليات الأساسية للشركات واستخدامات حياتنا اليومية مثل الترجمة الآلية للغات والتشخيص الطبي البحث الصوتي والبحث عن أقصر الطرق للوصول إلى وجهة ما وغيرها الكثير من التطبيقات ومن المتوقع أن ينمو السوق العالمي للتعلم الآلي من 8.43 مليار دولار أمريكي في عام 2019 إلى 117.19 مليار دولار أمريكي بحلول عام 2027 وفقًا لتوقع Fortune Business Insights. وقد بدأت العديد من الشركات بالفعل في استخدام خوارزميات التعلم الآلي نظرًا لقدرتها على إجراء تنبؤات وقرارات تجارية أكثر دقة ففي عام 2020 تم رصد 3.1 مليار دولار لتمويل شركات تعلم الآلة، وذلك لقدرة تعلم الآلة على إحداث تغييرات كبيرة في الصناعات. فيما يلي بعض التوقعات لمستقبل التعلم الآلي: يمكن للحوسبة الكمية أن تحدد مستقبل تعلم الآلة: والحوسبة الكمومية هي أحد المجالات التي لديها القدرة على تعزيز قدرات التعلم الآلي وهي تسمح بأداء عمليات متعددة الحالات في وقت واحد، مما يتيح معالجة أسرع للبيانات ففي عام 2019 أجرى معالج كمي خاص بشركة جوجل Google مهمة في 200 ثانية من شأنها أن تستغرق 10000 عام لإكمالها على أفضل حاسب عملاق في العالم! قد يتمكن برنامج AutoML من تسهيل عملية تطوير نماذج تعلم الآلة من البداية إلى النهاية: يعمل AutoML الذي طورته جوجل على أتمتة عملية بناء وتطبيق خوارزميات التعلم الآلي وهو يسمح لأي شخص أو شركة تجارية بتطبيق نماذج وتقنيات التعلم الآلي المعقدة وتخصيصها وفق احتياجات أعمالهم خلال دقائق دون أن يكون خبيرًا في التعلم الآلي. دخول تعلم الآلة بقوة في عملية التصنيع: لا زالت الشركات المصنعة ما زالت في المراحل الأولى فقط من تبني التعلم الآلي. ففي عام 2020 استفادت 9٪ فقط من الشركات المشاركة في استطلاع رأي حول استخدام الذكاء الاصطناعي في عملياتهم التجارية، لكن مع التطورات المستمرة التي نشهدها في مجال تعلم الآلة يتوقع أن تزداد هذه النسبة بشكل كبير ويصبح الاعتماد على استخدام تقنيات تعلم الآلة والروبوتات في أماكن التصنيع أكثر بكثير في المستقبل القريب. السيارات والمركبات ذاتية القيادة: تعمل شركات مثل تسلا وهوندا باستمرار على إنتاج هذا النوع من السيارات مستفيدة من تقنيات التعلم الآلي، وقد قدمت هذه الشركات بالفعل سيارات مؤتمتة جزئيًّا لكن لا تزال السيارات ذاتية القيادة قيد التطوير ولم تصل إلى الشكل النهائي. الخلاصة قدمنا من خلال هذه المقالة دليلًا شاملًا عن تعلم الآلة وبدأنا بتوضيح المفهوم نفسه وأنواع التعلم الآلي وتطبيقاته والفروقات بينها والفرق بين التعلم الآلي والذكاء الاصطناعي، كما تعرفنا على أهمية لغة البرمجة بايثون في مجال التعلم الآلي وسردنا أهم المكتبات وأطر العمل التي توفرها في مجال التعلم الآلي وقدّمنا مثلًا برمجيًا لتطبيق خوارزمية الانحدار اللوجستي كما سردنا أهم المصادر والمراجع لتعلم مجال الذكاء الاصطناعي عمومًا وتعلم الآلة خصوصًا ثم ألقينا نظرة أخيرًا على مستقل مجال تعلم الآلة. نرجو أن تكون هذه المقالة قد قدمت لك فهمًا جيّدًا لمجال تعلم الآلة ووضحت لك المفاهيم الأساسية لتنطلق في رحلة تعلم الآلة والانتقال إلى مواضيع وتطبيقات عملية حتى دخول سوق العمل.
  11. المؤشّر هو عنوان يشير إلى موقع في الذاكرة، وتُستخدم المؤشرات عادةً للسماح للدوالّ أو هياكل البيانات بالحصول على معلومات عن الذاكرة وتعديلها دون الحاجة إلى نسخ الذاكرة المشار إليها، والمؤشّرات قابلة للاستخدام سواءٌ مع الأنواع الأوليّة (المٌضمّنة) أو الأنواع التي يعرّفها المستخدم. تتضمن البرامج المكتوبة بلغة جو عادةً دوالًا وتوابعًا Methods نمرر لها بيانات مثل وسطاء، ونحتاج أحيانًا إلى إنشاء نسخة محلية من تلك البيانات بحيث تظل النسخة الأصلية من البيانات دون تغيير. مثلًا، لو كان لديك برنامج يمثّل مصرفًا bank، وتريد أن يُظهر هذا البرنامج التغيرات التي تطرأ على رصيد المستخدم تبعًا للخطة التوفيرية التي يختارها، فهنا قد تحتاج إلى بناء دالة تنجز هذا الأمر من خلال تمرير الرصيد الحالي للمستخدم إضافةً إلى الخطة التي يريدها. لا نريد هنا تغيير الرصيد الأساسي وإنما نريد فقط إظهار التعديل الذي سيطرأ على الرصيد، وبالتالي يجب أن نأخذ نسخةً من رصيد المستخدم مثل وسيط للدالة ونعدّل على هذه النسخة. تسمى هذه النسخة نسخةً محلية، وندعو عملية التمرير هذه "التمرير بالقيمة passing by value" لأننا لانرسل المتغير نفسه وإنما قيمته فقط. هناك حالات أخرى قد تحتاج فيها إلى تعديل البيانات الأصلية؛ بمعنى آخر قد نحتاج إلى تغيير قيمة المتغير الأصلي مباشرةً من خلال الدالة، فمثلًا، عندما يودع المستخدم رصيدًا إضافيًّا في حسابه، فهنا تحتاج إلى جعل الدالة قادرةً على تعديل قيمة الرصيد الأصلي وليس نسخةً منه (نحن نريد إضافة مال إلى رصيده السابق). ليس ضروريًا هنا تمرير البيانات الفعلية إلى الدالة، إذ يمكنك ببساطة إخبار الدالة بالمكان الذي توجد به البيانات في الذاكرة من خلال "مؤشر" يحمل عنوان البيانات الموجودة في الذاكرة. لا يحمل المؤشر القيمة، وإنما فقط عنوان أو مكان وجود القيمة، وتتمكن الدالة من خلال هذا المؤشر من التعديل على البيانات الأصلية مباشرةً. يسمى هذا "التمرير بالمرجع passing by reference"، لأن قيمة المتغير لا تُمرّر إلى الدالة، بل إلى موقعها فقط. سننشئ في هذا المقال المؤشرات ونستخدمها لمشاركة الوصول إلى الذاكرة المُخصصة لمتغير ما. تعريف واستخدام المؤشرات يوجد عنصرا صيغة مختلفان يختصان باستخدام مؤشّر لمتغيرٍ variable ما، وهما: معامل العنونة Address-of operator وهو "&" الذي يعيد عنوان المتغيّر الذي يوضع أمامه في الذاكرة، ومعامل التحصيل Dereference، وهو "*" الذي يعيد قيمة المتغير الموجود في العنوان المحدّد بواسطة عامله. ملاحظة: يُستخدم رمز النجمة * أيضًا للتصريح عن مؤشّر لمجرد التوضيح بأنه مؤشر، ولا ينبغي أن تخلط بينه وبين عامل التحصيل الذي يُستخدم للحصول على القيمة الموجودة في عنوان محدّد، فهما شيئان مختلفان مُمثّلان بنفس الرمز. مثال: var myPointer *int32 = &someint صرّحنا هنا عن متغير يُسمّى myPointer يُمثّل مؤشرًا لمتغير من نوع العدد الصحيح int32، وهيأنا المؤشر بعنوان someint، فالمؤشر هنا يحمل عنوان المتغير int32 وليس قيمته. دعنا نلقي الآن نظرةً على مؤشر لسلسلة، إذ تُصرّح الشيفرة التالية عن متغير يُمثّل سلسلة ومتغير آخر يُمثّل مؤشرًا على تلك السلسلة: package main import "fmt" func main() { var creature string = "shark" var pointer *string = &creature fmt.Println("creature =", creature) fmt.Println("pointer =", pointer) } شغّل البرنامج بالأمر التالي: $ go run main.go سيطبع البرنامج عند تشغيله قيمة المتغير إضافةً إلى عنوان تخزين المتغير (عنوان المؤشر). عنوان الذاكرة هو سلسلة أرقام مكتوبة بنظام العد السداسي عشري لأسباب عتادية وبرمجية لا تهمنا الآن، وطبعنا القيم هنا للتوضيح فقط. ستلاحظ تغيُّر العنوان المطبوع في كل مرة تُشغّل فيها البرنامج، لأنه يُهيّأ من جديد وتأخذ فيه المتغيرات أماكن غير محددة في الذاكرة؛ فكل برنامج ينشئ مساحته الخاصة من الذاكرة عند تشغيله. إذًا، سيكون خرج الشيفرة السابقة مختلفًا لديك عند تشغيله: creature = shark pointer = 0xc0000721e0 عرّفنا المتغير الأول creature من النوع string وهيّأناه بالقيمة shark. أنشأنا أيضًا متغيرًا يُسمّى pointer يُمثّل مؤشرًا على عنوان متغير سلسلة نصية، أي يحمل عنوان متغير نوعه string، وهيّأناه بعنوان السلسلة النصية المُمثّلة بالمتغير creature وذلك من خلال وضع المعامل "&" قبل اسمه. إذًا، سيحمل pointer عنوان الذاكرة التي يوجد بها creature وليس قيمته. هذا هو السبب وراء الحصول على القيمة 0xc0000721e0 عندما طبعنا قيمة المؤشر، وهو عنوان مكان تخزين متغير creature حاليًا في ذاكرة الحاسب. يمكنك الوصول إلى قيمة المتغير مباشرةً من خلال نفس المؤشر باستخدام معامل التحصيل "*" كما يلي: package main import "fmt" func main() { var creature string = "shark" var pointer *string = &creature fmt.Println("creature =", creature) fmt.Println("pointer =", pointer) fmt.Println("*pointer =", *pointer) } ويكون الخرج على النحو التالي: creature = shark pointer = 0xc000010200 *pointer = shark يُمثّل السطر الإضافي المطبوع ماتحدّثنا عنه (الوصول لقيمة المتغير من خلال المؤشر). نسمي ذلك "التحصيل" إشارةً إلى الوصول لقيمة المتغير من خلال عنوانه. يمكننا استخدام هذه الخاصية أيضًا في تعديل قيمة المتغير المُشار إليه: package main import "fmt" func main() { var creature string = "shark" var pointer *string = &creature fmt.Println("creature =", creature) fmt.Println("pointer =", pointer) fmt.Println("*pointer =", *pointer) *pointer = "jellyfish" fmt.Println("*pointer =", *pointer) } وسيكون الخرج على النحو التالي: creature = shark pointer = 0xc000094040 *pointer = shark *pointer = jellyfish لاحظ أننا في السطر "pointer = "jellyfish* وضعنا معامل التحصيل * قبل المؤشر للإشارة إلى أننا نريد تعديل القيمة التي يُشير إلى عنوانها المؤشر. أسندنا القيمة "jellyfish" إلى موقع الذاكرة التي يُِشير لها pointer، وهذا يُكافئ تعديل قيمة المتغير creature. لاحظ أنه عند طباعة القيمة التي يُشير لها المؤشر سنحصل على القيمة الجديدة. كما ذكرنا؛ فهذا يُكافئ تعديل قيمة المتغير creature، وبالتالي لو حاولنا طباعة قيمة المتغير creature سنحصل على القيمة "jellyfish" لأننا نُعدّل على الموقع الذاكري نفسه. سنضيف الآن سطرًا يطبع قيمة المتغير creature إلى الشيفرة السابقة: package main import "fmt" func main() { var creature string = "shark" var pointer *string = &creature fmt.Println("creature =", creature) fmt.Println("pointer =", pointer) fmt.Println("*pointer =", *pointer) *pointer = "jellyfish" fmt.Println("*pointer =", *pointer) fmt.Println("creature =", creature) } وسيكون الخرج كما يلي: creature = shark pointer = 0xc000010200 *pointer = shark *pointer = jellyfish creature = jellyfish يهدف كل ما تعلمته حتى الآن إلى توضيح فكرة المؤشرات في لغة جو وليس حالات الاستخدام الشائعة لها، فهي تُستخدم غالبًا عند تعريف وسطاء الدوال والقيم المُعادة منها أو عند تعريف التوابع مع أنواع مخصصة. دعنا الآن نلقي نظرةً على كيفية استخدام المؤشرات مع الدوال لمشاركة الوصول إلى المتغيرات. ضع في الحسبان أننا نطبع قيمة المؤشر لتوضيح أنه مؤشر، فلن تستخدم عمليًا قيمة المؤشر إلا للإشارة إلى القيمة الأساسية لاستردادها أو تعديلها. مستقبلات مؤشرات الدوال عند كتابة دالة، يمكنك تعريف بعض الوسطاء لكي تُمررهم لها إما بالقيمة أو بالمرجع؛ فعندما تُمرر وسيطًا ما بالقيمة، فهذا يعني أنك تُرسل نسخةً مُستقلة من قيمة هذا الوسيط إلى الدالة، وبالتالي فإن أي تغيير يحدث لهذه النسخة لن يؤثر على النسخة الأساسية من البيانات، لأن كل التعديلات ستجري على نُسخة من البيانات؛ أما عندما تُمرر وسيطًا بالمرجع، فهذا يعني أنك تُرسل مؤشرًا يحمل عنوان ذلك الوسيط -أي مكان تواجد البيانات في الذاكرة- إلى الدالة، وبالتالي أصبح لديك القدرة على الوصول إلى البيانات الأصلية من داخل الدالة والتعديل عليها مباشرةً. يمكنك الاطلاع على المقالة التالية إذا أردت معرفة المزيد عن الدوال وطرق استدعائها في لغة جو. يمكنك طبعًا تمرير أي وسيط بالطريقة التي تُريدها (بالقيمة أو بالمرجع)، فهذا يعتمد على ما تحتاجه؛ فإذا كنت تريد أن تُعدّل الدالة على البيانات الأصلية نُمرر الوسيط بالمرجع وإلا بالقيمة. لمعرفة الفرق بدقة، دعنا أولًا نلقي نظرة على دالة تمرّر وسيطًا بالقيمة: package main import "fmt" type Creature struct { Species string } func main() { var creature Creature = Creature{Species: "shark"} fmt.Printf("1) %+v\n", creature) changeCreature(creature) fmt.Printf("3) %+v\n", creature) } func changeCreature(creature Creature) { creature.Species = "jellyfish" fmt.Printf("2) %+v\n", creature) } ويكون الخرج على النحو التالي: 1) {Species:shark} 2) {Species:jellyfish} 3) {Species:shark} أنشأنا بدايةً نوع بيانات مخصص أسميناه Creature، يحتوي على حقل واحد يُسمى Species من نوع سلسلة نصية string، وأنشأنا داخل الدالة الرئيسية main متغير من النوع Creature اسمه creature وأسندنا السلسلة shark إلى الحقل Species. بعد ذلك طبعنا المتغير creature لإظهار القيمة التي يتضمنها في الوقت الحالي، ثم مرّرنا المتغير creature (تمرير بالقيمة أي نُسخة) إلى الدالة changeCreature والتي بدورها تطبع قيمة المتغير المُمرر لها بعد إسناد السلسلة "jellyfish" إلى الحقل Species (هنا نطبعه من داخل الدالة أي محليًّا). بعد ذلك طبعنا قيمة المتغير creature مرةً أخرى (خارج الدالة السابقة). لاحظ أنه يوجد لدينا ثلاث تعليمات طباعة؛ جرى السطر الأول والثالث من الخرج ضمن نطاق الدالة main بينما كان السطر الثاني ضمن نطاق الدالة changeCreature. لاحظ أيضًا أنه في البداية كانت قيمة المتغير creature هي "shark" وبالتالي عند تنفيذ تعليمة الطباعة الأولى سيطبع: (1) {Species:shark} أما تعليمة الطباعة في السطر الثاني والموجودة ضمن نطاق الدالة changeCreature، فسنلاحظ أنها ستطبع القيمة: (2) {Species:jellyfish} لأننا عدلنا قيمة المتغير، أما في التعليمة الثالثة فقد يُخطئ البعض ويعتقد أنها ستطبع نفس القيمة التي طبعتها تعليمة السطر الثاني، لكن هذا لا يحدث لأن التعديل بقي محليًّا ضمن نطاق الدالة changeCreature، أي حدث التعديل على نسخة من المتغير creature وبالتالي لا ينتقل التعديل إلى المتغير الأساسي. إذًا سيكون خرج تعليمات الطباعة للسطرين الأول والثالث متطابق. سنأخذ الآن نفس المثال، لكن سنغيّر عملية التمرير إلى الدالة changeCreature لتصبح تمرير بالمرجع، وذلك من خلال تغيير النوع من creature إلى مؤشر باستخدام المعامل "*"، فبدلًا من تمرير creature، سنمرّر الآن مؤشرًا إلى creature أو creature*. كان creature في المثال السابق من النوع struct ويحتوي قيمة الحقل Species وهي "shark"، أما creature* فهو مؤشر وليس struct، وبالتالي قيمته هي موقع الذاكرة وهذا ما مرّرناه إلى الدالة ()changeCreature. لاحظ أننا نضع المعامل "&" عند تمرير المتغير creature إلى الدالة. package main import "fmt" type Creature struct { Species string } func main() { var creature Creature = Creature{Species: "shark"} fmt.Printf("1) %+v\n", creature) changeCreature(&creature) fmt.Printf("3) %+v\n", creature) } func changeCreature(creature *Creature) { creature.Species = "jellyfish" fmt.Printf("2) %+v\n", creature) } ستحصل عند تنفيذ الشيفرة السابقة على الخرج التالي: 1) {Species:shark} 2) &{Species:jellyfish} 3) {Species:jellyfish} قد تبدو الأمور واضحة الآن، فعندما مرّرنا المتغير creature إلى الدالة changeCreature، كان التمرير بالمرجع، وبالتالي أي تغيير يطرأ على المتغير creature (وهو تغيير قيمة الحقل Species إلى "jellyfish") داخل هذه الدالة، سيكون مُطبّقًا على المتغير الأصلي نفسه الموجود ضمن الدالة main لأننا نُعدّل على نفس الموقع في الذاكرة، وبالتالي ستكون قيمة الخرج لتعليمات الطباعة 2 و 3 مُتطابقة. قد لا يكون لدينا في بعض الأحيان قيمة مُعرّفة للمؤشر، وهذا قد يحدث لأسباب كثيرة منها ما هو متوقع ومنها لا، وبالتالي قد يسبب لك حالات هلع panic في البرنامج. دعنا نلقي نظرةً على كيفية حدوث ذلك وكيفية التخطيط لتلك المشكلة المحتملة. التأشير إلى اللاشيء Nil القيمة الافتراضية لجميع المتغيرات في لغة جو هي الصفر، وهذا الكلام ينطبق أيضًا على المؤشرات. لدى التصريح عن مؤشر بنوعٍ ما ولكن دون أي قيمة مُسندة، ستكون القيمة الصفرية الافتراضية هي nil. الصفر هنا مفهوم متعلق بالنوع، أي أنه في حالة الأعداد الصحيحة هو العدد 0، وفي حالة السلاسل النصية هو السلسلة الفارغة ""، وأخيرًا في حالة المؤشرات هو القيمة nil إشارةً إلى الحالة الافتراضية لقيمة أي مؤشر. سنُعدّل في البرنامج التالي على البرنامج السابق، بحيث نعرّف مؤشرًا متغيرًا creature من النوع Creature، لكن دون استنساخ للنسخة الحقيقية من Creature ودون إسناد عنوانها إلى المؤشر؛ أي أن قيمة المؤشر هي nil، ولن نستطيع الرجوع إلى أي من الحقول أو التوابع المُعرّفة في النوع Creature. لنرى ماذا سيحدث: package main import "fmt" type Creature struct { Species string } func main() { var creature *Creature fmt.Printf("1) %+v\n", creature) changeCreature(creature) fmt.Printf("3) %+v\n", creature) } func changeCreature(creature *Creature) { creature.Species = "jellyfish" fmt.Printf("2) %+v\n", creature) } سيكون الخرج على النحو التالي: 1) <nil> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86] goroutine 1 [running]: main.changeCreature(0x0) /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18 +0x26 main.main() /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13 +0x98 exit status 2 نلاحظ عند تشغيل البرنامج أن تعليمة الطباعة الأولى 1 نجحت وطبعت قيمة المتغير creature وهي <nil>، لكن عندما وصلنا إلى استدعاء الدالة changeCreature ومحاولة ضبط قيمة الحقل Species، ظهرت حالة هلع في البرنامج نظرًا لعدم إنشاء نسخة من هذا المتغيّر، وأدى هذا إلى محاولة الوصول إلى موقع ذاكري غير موجود أصلًا أو غير مُحدد. هذا الأمر شائع في لغة جو، لذلك عندما تتلقى وسيطًا مثل مؤشر، لا بُد من فحصه إذا كان فارغًا أم لا قبل إجراء أي عمليات عليه، لتجنب حالات هلع كهذه. نتحقق عادةً من قيمة المؤشر في أي دالة تستقبل مؤشرًا مثل وسيط لها كما يلي: if someVariable == nil { // هنا يمكن أن نطبع أي رسالة تُشير إلى هذه الحالة أو أن نخرج من الدالة } يمكنك بذلك التحقق مما إذا كان الوسيط يحمل قيمةً صفريةً أم لا. عند تمرير قيمة صفرية (هنا نقصد nil) قد ترغب في الخروج من الدالة باستخدام تعليمة return أو إعادة رسالة خطأ تخبر المستخدم أن الوسيط الممرّر إلى الدالة أو التابع غير صالح. تتحقق الشيفرة التالية من وجود قيمة صفرية للمؤشر: package main import "fmt" type Creature struct { Species string } func main() { var creature *Creature fmt.Printf("1) %+v\n", creature) changeCreature(creature) fmt.Printf("3) %+v\n", creature) } func changeCreature(creature *Creature) { if creature == nil { fmt.Println("creature is nil") return } creature.Species = "jellyfish" fmt.Printf("2) %+v\n", creature) } أضفنا إلى الدالة changeCreature تعليمات لفحص قيمة الوسيط creature فيما إذا كانت صفرية أم لا؛ ففي حال كانت صفرية نطبع "creature is nil" ونخرج من الدالة من خلال تعليمة return، وإلا نتابع العمل في الدالة ونُعدّل قيمة الحقل Species. سنحصل الآن على المخرجات التالية: 1) <nil> creature is nil 3) <nil> لاحظ أنه على الرغم من وجود حالة صفرية للمتغير، إلا أنه لم تحدث حالة هلع للبرنامج لأننا عالجناها. إذا أنشأنا نسخةً من النوع Creature وأُسندت للمتغير creature، سيتغير الخرج بالتأكيد، لأنه أصبح يُشير إلى موقع ذاكري حقيقي: package main import "fmt" type Creature struct { Species string } func main() { var creature *Creature creature = &Creature{Species: "shark"} fmt.Printf("1) %+v\n", creature) changeCreature(creature) fmt.Printf("3) %+v\n", creature) } func changeCreature(creature *Creature) { if creature == nil { fmt.Println("creature is nil") return } creature.Species = "jellyfish" fmt.Printf("2) %+v\n", creature) } سنحصل على الناتج المتوقع التالي: 1) &{Species:shark} 2) &{Species:jellyfish} 3) &{Species:jellyfish} عندما تتعامل مع المؤشرات، هناك احتمال أن يتعرّض البرنامج لحالة هلع، لذلك يجب عليك التحقق لمعرفة ما إذا كانت قيمة المؤشر صفرية قبل محاولة الوصول إلى أي من الحقول أو التوابع المعرّفة ضمن نوع البيانات الذي يشير إليه. دعنا نلقي نظرةً على كيفية استخدام المؤشرات مع التوابع. مستقبلات مؤشرات التوابع المُستقبل receiver في لغة جو هو الوسيط الذي يُعرّف عند التصريح عن التابع. ألقِ نظرةً على الشيفرة التالية: type Creature struct { Species string } func (c Creature) String() string { return c.Species } المُستقبل في هذا التابع هو c Creature، وهو يُشير إلى أن نسخة المتغير c من النوع Creature وأنك ستستخدمه ليُشير إلى نسخة متغير من هذا النوع. يختلف أيضًا سلوك التوابع كما هو الحال في الدوال التي يختلف فيها سلوك الدالة تبعًا لطريقة تمرير الوسيط (بالمرجع أو بالقيمة). ينبع الاختلاف الأساسي من أنه إذا صرّحت عن دالة مع مُستقبل "قيمة"، فلن تتمكن من إجراء تغييرات على نسخة هذا النوع التي عُرّف التابع عليه. عمومًا، ستكون هناك أوقات تحتاج فيها أن يكون تابعك قادرًا على تحديث نسخة المتغير الذي تستخدمه، ولإجراء هكذا تحديثات ينبغي عليك جعل المستقبل "مؤشرًا". دعنا نضيف التابع Reset للنوع Creature، الذي يسند سلسلةً نصيةً فارغة إلى الحقل Species. package main import "fmt" type Creature struct { Species string } func (c Creature) Reset() { c.Species = "" } func main() { var creature Creature = Creature{Species: "shark"} fmt.Printf("1) %+v\n", creature) creature.Reset() fmt.Printf("2) %+v\n", creature) } إذا شغّلت البرنامج ستحصل على الخرج: 1) {Species:shark} 2) {Species:shark} لاحظ أنه على الرغم من ضبطنا قيمة الحقل Species على السلسلة الفارغة في التابع Reset، إلا أننا عندما طبعنا المتغير creature في الدالة main حصلنا على "shark". السبب في عدم انتقال التغيير هو استخدامنا مُستقبل قيمة في تعريف التابع Reset، وبالتالي سيكون لهذا التابع إمكانية التعديل فقط على نسخة المتغير creature وليس المتغير الأصلي. بالتالي، إذا أردنا تحديث هذه القيمة؛ أي التعديل على النسخة الأصلية للمتغير، فيجب علينا تعريف مُستقبل مؤشر. package main import "fmt" type Creature struct { Species string } func (c *Creature) Reset() { c.Species = "" } func main() { var creature Creature = Creature{Species: "shark"} fmt.Printf("1) %+v\n", creature) creature.Reset() fmt.Printf("2) %+v\n", creature) } لاحظ أننا أضفنا المعامل "*" أمام النوع Creature عندما صرّحنا عن التابع Reset، وهذا يعني أن الوسيط الذي نُمرره أصبح مؤشرًا، وبالتالي أصبحت كل التعديلات التي نُجريها من خلاله مُطبّقةً على المتغير الأصلي. 1) {Species:shark} 2) {Species:} لاحظ أن التابع Reset عدّل قيمة الحقل Species كما توقعنا، وهذا مُماثل لفكرة التمرير بالمرجع أو القيمة في الدوال. خاتمة تؤثر طريقة تمرير الوسطاء (بالمرجع أو القيمة) إلى التوابع أو الدوال على آلية الوصول إلى المتغير المُمرّر؛ ففي حالة التمرير بالمرجع يكون التعديل مباشرةً على المتغير الأصلي، أما في الحالة الثانية فيكون على نسخة من المتغير. الآن بعد أن تعرفت على المؤشرات، أصبح بإمكانك التعرف على استخدامها مع الواجهات أيضًا. ترجمة -وبتصرف- للمقال Understanding Pointers in Go لصاحبه Gopher Guides. المقال السابق: استخدام وسوم البناء لتخصيص الملفات التنفيذية Binaries في لغة جو Go المؤشرات Pointers في لغة سي C المؤشرات Pointers في لغة Cpp
  12. إذا كنت ترغب في تَعلُّم الذكاء الاصطناعي أو العمل كمهندس ذكاء صناعي لكنك مشتت في الخطوات التي عليك اتباعها ولا تزال المصطلحات والمفاهيم المرتبطة بهذا العلم مبهمة بالنسبة لك وتراودك تساؤلات من قبيل: هل يمكنني تعلم الذكاء الاصطناعي حتى لو لم يكن لدي خبرة في البرمجة؟ أريد تعلم الذكاء الاصطناعي لكني لا أعرف من أين أبدأ؟ ما هي خطوات تعلم الذكاء الاصطناعي؟ ما هي أهم مصادر تعلم الذكاء الاصطناعي؟ إذا كانت الإجابة نعم فهذه المقالة ستفيدك حتمًا حيث سأوفر لك فيها إجابة شافية على كل ما سبق، وأحاول أن أبسط الأمور قدر المستطاع وأشرح لك بأسلوب موجز ماهية الذكاء الاصطناعي وأهميته وأهم تطبيقاته، وأوضح لك الخطوات التي عليك اتباعها من أجل تعلم الذكاء الاصطناعي، وأهم التخصصات وفرص العمل المرتبطة بهذا المجال، ثم أختم المقال بالإجابة على على أكثر الأسئلة شيوعًا في مجال الذكاء الاصطناعي. وأنوه لأن المقالة الحالية هي جزء من سلسلة مقالات عن الذكاء الصناعي بدأناها مع مقدمة شاملة عن الذكاء الاصطناعي ما هو الذكاء الاصطناعي؟ الذكاء الاصطناعي أو الذكاء الصناعي Artificial Intelligence أو اختصارًا AI هو العلم الذي يعطي يجعل الآلات قادرة على اتخاذ قرارات والتصرف بذكاء من خلال محاكاة البشر وطريقتهم في التفكير، فنحن البشر نحصل على المعلومات الواردة من العالم الخارجي ونعالجها في عقولنا ونصدر الأحكام والاستنتاجات بناء عليها وبناء على تجاربنا السابقة. يمكنك تشبيه عملية الذكاء الاصطناعي وتعلم الآلة بالمولود الجديد الذي لا يستطيع تعلم أو عمل أي شيء بمفرده إذا لم يعلمه والداه ويدربانه وينقلان له المعرفة ويمكنانه من التعرف على ما هو خطأ وما هو صواب، وبذلك تتعلم الآلة وتصبح قادرة على اتخاذ القرارات وإعطاء الاستنتاجات واقتراح الحلول. نعرف أن الآلات هي مجرد عتاد قابل للبرمجة ولكن في عصر الذكاء الاصطناعي أصبحت قابلة للتعلم أيضًا بطريقة تمكنها من إصدار أحكام وقرارات مشابهة للبشر من خلال اتباع طريقة معينة في البرمجة تسمى تعلم الآلة Machine Learning وهي مصطلح مرافق للذكاء الاصطناعي يُمكِّن الآلات من التعلم من أكوام من البيانات بتطبيق خوارزميات ونماذج وأنماط مسبقة البناء عليها نعطيها لها مع البيانات لتستنتج بذلك منها المعلومات دون أن تتم برمجتها وتعليمها بشكل صريح وبذلك تتعلم الآلات وتصبح أكثر ذكاءً. مثلًا بسيط على ما سبق هو تعليم الطفل الصغير أصناف الحيوانات فنقول له الطائر يطير وله جناحان وهذا هو النموذج الحاكم، وبذلك يستطيع الطفل تطبيق هذا النموذج لتصنيف الطيور عن غيرها من أصناف الحيوانات الأخرى مهما أعطيناه من أصناف وأشكال مختلفة، وهو بالضبط ما نفعله مع الآلات باختصار وتبسيط. أهمية تعلم الذكاء الاصطناعي قد تتساءل لماذا أتعلم الذكاء الاصطناعي؟ وهل هناك فائدة من تعلمه والجواب هو نعم بكل تأكيد فالذكاء الصناعي واحد من أهم العلوم الحديثة المستخدمة في عملية إيجاد حلول لمشاكل المجتمع المُلحّة والمصيرية، كالأمراض ومشاكل التلوث وتغير المناخ والعمل في البيئات الخطرة (كالبحر والفضاء). كما يعد الذكاء الاصطناعي أيضًا قطاعًا اقتصاديًّا سريع النمو، حيث من المتوقع أن تزيد عائدات البرامج التي تعتمد على تقنيات الذكاء الاصطناعي بنسبة 21.3٪ عن العام السابق، بإجمالي 62.5 مليار دولار في عام 2022. إذًا من خلال تعلّم الذكاء الاصطناعي، لن تكون مواكبًا لثورة هذا العصر فقط بل ستكون جزءًا منها، عدا عن كون العمل بها ممتع ومحطّ جذب لمن يُريد تحقيق إنجازات علمية وبحثية. أضف إلى ذلك يعتبر العمل في مجال الذكاء الصناعي مُجزيًّا ماديًّا، فمتوسط الرواتب لمهندسي الذكاء الاصطناعي وفقًا لموقع Glassdoor المتخصص في الولايات المتحدة الأمريكية يزيد عن 127 ألف دولارًا أمريكيًّا في السنة، بينما يبلغ متوسط الأجر لمهندس الذكاء الاصطناعي في الإمارات العربية المتحدة 337 ألف درهمًا إماراتيًا (حوالي 94 ألف دولارًا أمريكيًّا) في السنة وفقًا للموقع المتخصص erieri. وعلى الرغم من أن قدرات الذكاء الصناعي لم تأخذ ذروتها بعد ومازالت في مهدها، إلا أنّه أصبح جزءًا أساسيًّا في الكثير من التطبيقات المختلفة، ويتزايد عدد الشركات التي تعتمد الذكاء الاصطناعي لتحسين أدائها ما يعني أن الوظائف المرتبطة بالذكاء الاصطناعي تُصبح مطلوبة كثيرًا وبوتيرة متسارعة. فبحسب استطلاع أجرته شركة McKinsey أظهر أن الذكاء الصناعي يستخدم بوتيرة متزايدة في تحسين الخدمات وتعزيز المنتجات ونمذجة المخاطر ومنع الاحتيال، واعتبارًا من الآن وحتى عام 2030 من المتوقع أن ينمو الطلب على وظائف أبحاث الحاسب والمعلومات بنسبة 22٪. وعلى أهمية هذا المجال وانتشاره لا يزال هناك نقص في الموارد البشرية للذكاء الاصطناعي لاسيما في العالم العربي، فإذا كنت شخصًا مهتمًا بالذكاء الاصطناعي وترغب في تعلمه فأنت تمنح نفسك فرصة عظيمة للحصول على عمل يبشر بمستقبل واعد وفرص واسعة. تطبيقات الذكاء الاصطناعي قد يخطر ببالك عند سماع مصطلح الذكاء الاصطناعي أنه يطبق فقط في الروبوتات المتقدمة التي نراها في البرامج الوثائقية وأفلام الخيال العلمي لكن هذا المثال عام جدًا فالذكاء الاصطناعي يمكن أن يطبق في العديد من المجالات ويصمم لإنجاز مهمة محددة في مجال الرعاية الصحية أو الخدمات المصرفية أو التعليم أو الترفيه أو الإدارة وما إلى ذلك. ومن أبرز تطبيقات الذكاء الصناعي نذكر: التجارة الإلكترونية: يمكن تطبيق الذكاء الاصطناعي للتوصية بأفضل المنتجات واستخدام روبوتات الدردشة ومنع عمليات الاحتيال على بطاقة الائتمان. التعليم: يمكن الاستفادة من الذكاء الاصطناعي في وضع خطط دراسية متكاملة وإنشاء محتوى تعليمي ذكي وأتمتة المهام الإدارية. الرعاية الصحية: يستخدم الذكاء الاصطناعي في التعرف على الأمراض المختلفة وتشخيصها والكشف المبكر عنها وفي اكتشاف أدوية جديدة. السيارات: يستخدم الذكاء الاصطناعي في تصنيع السيارات الذكية وذاتية القيادة كما يحسن تجربة القيادة في السيارة من خلال توفير أنظمة إضافية مثل كبح الفرامل عند الطوارئ. وسائل التواصل الاجتماعي: يمكن الاستفادة من الذكاء الاصطناعي للتوصية بالمنشورات التي قد يهتم بها المتابعون بناء على نوع المحتوى الذي يتفاعلون معه والكشف عن المحتوى غير الملائم والترجمة التلقائية للمحتوى. كانت هذه أمثلة على بعض استخدامات الذكاء الاصطناعي، وتجدر الإشارة أن الذكاء الاصطناعي آخذ في التقدم دونما توقف وتضاف العديد من المجالات التي يضع الذكاء الصناعي بصمته فيها كالزراعة والنقل والفضاء والتجارة الإلكترونية وفي مقالة تطبيقات الذكاء الاصطناعي نشرنا مقالة شاملة لكل هذه المجالات. ما هي وظيفة مهندس الذكاء الاصطناعي؟ يسمى الشخص المسؤول عن الذكاء الاصطناعي داخل أي مؤسسة أو منظمة مهندس الذكاء الاصطناعي وهو منصب وظيفي مرموق، وقد تختلف مسؤوليات مهندس الذكاء الاصطناعي حسب مكان العمل ولكنه يكون مسؤولًا بشكل عام عن الأمور التالية: تطوير أنظمة وتطبيقات ذكية من شأنها تحسين الأداء واتخاذ قرارات أفضل. تحقيق أهداف العمل باستخدام أساليب الذكاء الاصطناعي. استخدام المنطق والاحتمالات وتعلم الآلة لحل مشكلات العمل. تطبيق أفضل الممارسات والخوارزميات لاستخراج البيانات حول المشتريات والمبيعات. والمنتجات ومعالجتها وتحليلها والتنبؤ بالأحداث المستقبلية. تحليل الأنظمة الحالية والعمل على مشاريع تطويرية. ويمكن لمهندس الذكاء الاصطناعي أن يتخصص في مجالات مختلفة مثل التعلم الآلي أو التعلم العميق وسنتحدث بشكل موسع عن هذه التخصصات في فقرة لاحقة. وكي تصبح مهندس ذكاء اصطناعي تحتاج لمجموعة من المهارات الشخصية أهمها الفكر التحليلي والإبداعي والتواصل الجيد والعمل بروح الفريق إلى جانب المهارات التقنية المتعددة التي ذكرناها وعلى رأسها الرياضيات والإحصاء وعلوم الحاسوب ولغات البرمجة. أساسيات الذكاء الاصطناعي قد تتمكن من صعود السلم درجتين درجتين لكن أكثر من ذلك سيكون الأمر مستحيلًا، لذا من الأفضل صعوده درجة درجة وقبل أن تبدأ في تعلم الذكاء الصناعي، يجب أن يكون لديك أساس في المجالات التالية وكلما كان أساسك أكبر كلما كان أفضل: مبادئ علوم الحاسب لغات البرمجة الخوارزميات وهياكل البيانات أنظمة التشغيل حل المشكلات وتطبيقها باستخدام البرمجة الحاسوبية لنتعرف سويًا على كل مجال من المجالات الواردة أعلاه بمزيد من التفصيل وأهميتها في تعلم الذكاء الاصطناعي مبادئ علوم الحاسب لا يمكنك لعب كرة القدم بدون كرة. الأمر ذاته ينطبق هنا، فلابد من فهم المبادئ الأساسية لعلوم الحاسب قبل أن تتمكن من بدء برمجة الذكاء الاصطناعي. هذا يتضمن (سنضع الحد الأدنى من مستوى المعرفة المطلوبة لكل منها): 1. برمجة الحاسب (متوسطة) البرمجة هي مجموعة الأنشطة التي تمكننا من التخاطب مع الحاسب وتوجيه الأوامر له، وتتضمن هذه الأنشطة أمورًا مثل كتابة التعليمات وتوجيه الأوامر وبناء الخوارزميات وكي تتمكن من برمجة الحاسب يجب أن تتعلم إحدى لغات البرمجة. 2. لغات البرمجة (أساسية) يجب أن يكون لديك القدرة على البرمجة بإحدى لغات البرمجة وتحديدًا لغة بايثون، وهي كافية لتطوير مختلف تطبيقات الذكاء الاصطناعي بمختلف تخصصاته. 3. الخوارزميات وهياكل البيانات (متوسطة) هياكل البيانات Data structure هي تنظيم وإدارة وتخزين البيانات بطريقة تتيح الوصول والتعديل الفعّال مثل المصفوفات Arrays، والقوائم المترابطة Linked list والمكدس Stack …إلخ. أما الخوارزمية فهي عبارة عن مجموعة من الخطوات لحل مشكلة معينة، ومن خلال فهم الخوارزميات وهياكل البيانات وكيفية بنائها، يمكننا كتابة برامج وتطبيقات فعّالة. 4. أنظمة التشغيل (أساسية) يجب أن يكون لديك مهارة أساسية في التعامل مع نظامي التشغيل ويندوز لا نقول أن تكون خبيرًا بهما بل يكفي تعلم الأمور الأساسية وتحديدًا كيفية استعمال سطر الأوامر وتشغيل الخدمات الخلفية وإيقافها وإدارة العمليات وغيرها. وننصحك هنا بالرجوع إلى كتاب أنظمة التشغيل للمبرمجين الذي يجب على كل مبرمج قراءته ليحيط بما يلزمه من معلومات وتفاصيل عن نظام التشغيل. 5. حل المشكلات وتطبيقها باستخدام البرمجة الحاسوبية (متوسطة) حل المشكلات هو عملية تحويل وصف المشكلة إلى حل باستخدام معرفتنا بمجال المشكلة والاعتماد على قدرتنا على اختيار واستخدام استراتيجيات وتقنيات وأدوات مناسبة لحل المشكلات. يمكنك الاطلاع على مقالة حل المشكلات وأهميتها في احتراف البرمجة. لمزيد من التفصيل، يمكنك الرجوع إلى مقال المدخل الشامل لتعلم علوم الحاسوب. الرياضيات الرياضيات هي أساس العلوم العلمية، وبالتأكيد هي الأساس الذي انطلق منه الذكاء الصناعي لذا ستحتاج إلى معرفة بعض أساسيات الرياضيات على الأقل للخوض في مجال الذكاء الاصطناعي والتي تشمل: الجبر الخطي التفاضلات التحليل العددي الرياضيات المتقطعة الاحتمالات والإحصاء تعمل الاحتمالات والإحصائيات كأساس للتحليل والتعامل مع البيانات وتقييم نماذج الذكاء الصناعي، كما أنها أساس للعديد من الخوارزميات فيه (خوارزمية بايز مثلًا). يمكن أن تجيب الاحتمالات والإحصاءات على أسئلة مثل: ما هي النتيجة الشائعة؟ ما هي النتيجة المتوقعة؟ كيف تبدو البيانات؟ وغيرها من الأسئلة. تعلم استعمال أدوات الذكاء الاصطناعي هناك العديد من أدوات الذكاء الصناعي التي يمكنك تعلمها والتي تساعدك في أتمتة عملك وتوفر وقتك في تطوير التطبيقات وزيادة كفاءتها ومن أشهرها: SciKit-Learn: التي تعد من أشهر مكتبات بايثون في مجال الذكاء الاصطناعي وهي مبنية على مكتبة SciPy التي توفر العديد من خوارزميات تعلم الآلة وهي تناسب المبتدئين وتسهل عملهم بشكل كبير. TensorFlow: مكتبة مفتوحة المصدر ومتوافقة مع بايثون طورتها جوجل وهي تسهل عمليات الحساب العددي وتستخدم في العديد من مهام الذكاء الاصطناعي وتعلم الآلة والشبكات العصبية. PyTorch: مكتبة بايثون طورتها فيسبوك وتسرع عملية تطوير تطبيقات الرؤية الحاسوبية ومعالجة اللغات الطبيعية. CNTK: تعد واحدة من أفضل أدوات الذكاء الاصطناعي من مايكروسوفت وهي مشابهة لـ TensorFlow وتملك العديد من واجهات برمجة التطبيقات API للغات بايثون وجافا و C++‎ و C Caffe: مكتبة بايثون مفتوحة المصدر تساعد في تطبيقات الذكاء الاصطناعي وتناسب بشكل خاص مشاريع البحث الأكاديمي والتطبيقات الصناعية المتقدمة لكونها تتميز بسرعة المعالجة. Keras: مكتبة مبنية على TensorFlow وتوفر واجهة بايثون وهي واحدة من أفضل أدوات الذكاء الاصطناعي مفتوحة المصدر المستخدمة اليوم. إلى جانب العديد من أدوات الذكاء الاصطناعي التي يمكنك تعلمها مثل OpenNN و Google ML kit و Theano و Swift AI و AutoML و H2O التي تصلح لتطبيقات متنوعة، اختر من بينها ما يناسب احتياجاتك. دورة الذكاء الاصطناعي احترف برمجة الذكاء الاصطناعي AI وتحليل البيانات وتعلم كافة المعلومات التي تحتاجها لبناء نماذج ذكاء اصطناعي متخصصة. اشترك الآن مجالات الذكاء الاصطناعي إذا قررت تعلم الذكاء الصناعي ستلاحظ أنه علم واسع ومتشعب ويتفرع عنه عدة مجالات أو علوم فرعية وفيما يلي نذكر لك من باب الاطلاع أهم هذه المجالات. تعلم الآلة Machine learning التعلم العميق Deep Learning معالجة اللغات الطبيعية Natural Language Processing علم الروبوتات Robotics الشبكات العصبية الاصطناعية Artificial Neural Networks المنطق الترجيحي أو الضبابي Fuzzy Logic الأنظمة الخبيرة Expert systems لا تقلق إن بدت لك التسميات مربكة وصعبة فبعد أن تنتهي من مرحلة تعلم الأساسيات وتبدأ بالتطبيق العملي ستتوضح لك هذه المصطلحات بشكل أفضل وإليك وصفًا سريعًا لكل مجال من هذه المجالات وأهم استخداماته وتطبيقاته: تعلم الآلة Machine learning يركز هذا المجال على بناء أنظمة تدرب الآلة على بيانات قديمة ومعروفة والاعتماد عليها من أجل إصدار التنبؤات والقرارات بناءً على بيانات جديدة لم يتم التدرب عليها مسبقًا فهو يمكّن الآلة من التعلم وتطوير نفسها من مجموعات تجارب السابقة دون الحاجة إلى برمجة صريحة. على سبيل المثال يستخدم تطبيق خرائط جوجل كافة البيانات السابقة للأشخاص الذين سلكوا مسارًا معينًا كي يتنبأ بحركة المرور القادمة على هذا المسار وتطبيق DeepFace التابع لفيسبوك والذي يتعرف على الوجوه ويحدد الأشخاص الموجودين في صورة ما بناء على صور سابقة مخزنة لهم. التعلم العميق Deep Learning التعلم العميق هو فرع أكثر تخصصًا من التعلم الآلي ففي التعلم الآلي تحدد الميزات التي نريد تصنيف البيانات بناء عليها بشكل مسبق أما في التعلم العميق تتولى الآلة عملية استخراج الميزات ذات الصلة تلقائيًا، وهو يستخدم بنية خاصة تسمى الشبكات العصبية والتي تتكون من عدة طبقات لذا يسمى تعليم عميق. في هذا المجال يتم تدريب الآلة على مجموعة كبيرة من البيانات المصنفة والتعرف على مميزاتها مباشرة والاعتماد عليها في تصنيف البيانات المستقبلية بدقة عالية، وهو يستخدم في عدة مجالات مثل السيارات ذاتية القيادة للتعرف على الكائنات المحيطة وتمييزها مثل المشاة وإشارات المرور. معالجة اللغات الطبيعية Natural Language Processing يهتم هذا الفرع من فروع الذكاء الاصطناعي على جعل الآلة قادرة على فهم لغاتنا البشرية الطبيعية، ومن الأمثلة الشهيرة على تطبيقات هذا الفرع برامج روبوتات الدردشة، لا بد وأنك قد سمعت في الآونة الأخيرة عن ChatGPT. علم الروبوتات Robotics هذا المجال يجمع بين الهندسة الميكانيكية والكهربائية والتقنية ويتخصص في تطوير الروبوتات. والروبوت هو آلة قابلة للبرمجة تؤدي وظائف تشبه إلى حد كبير وظائف البشر ويمكن استثمارها في أي صناعة تقريبًا وعلى أي مستوى من الاستخدام فهناك روبوتات تستخدم في صناعة السيارات والتنظيف والطب وغيرها. ملاحظة: هناك روبوتات برمجية تسمى برامج الروبوت وهي برامج حاسوبية تنفذ المهام بشكل مستقل ومثال عليها روبوتات الدردشة الذي ذكرناه سابقًا لكن هذه الروبوتات لا تندرج ضمن هذا المجال لكونها لا تملك هيكل مادي فهي تنشأ داخل جهاز حاسوب وتتواجد على شبكة الإنترنت فقط. الشبكات العصبية الاصطناعية Artificial Neural Networks الشبكات العصبية الاصطناعية أو اختصارًا الشبكات العصبية هي أحد الفروع التخصصية للتعلم العميق والشبكة العصبية ماهي إلا مجموعة من الخوارزميات المنفذة بشكل يحاكي الدماغ البشري والتي تتكون من مجموعة من (العقد المتصلة التي تسمى الخلايا العصبية الصناعية والتي تشبه الخلايا العصبية للدماغ البشري. عندما تتعلم طريقة استخدام الشبكات العصبية ستتمكن من بناء تطبيقات متنوعة مثل التعرف على الصور والأصوات والوجوه والتحقق من صحة التوقيع اليدوي لشخص ما وتستخدم كذلك في توقعات سوق الأسهم وفي الكشف عن الأمراض في صور الأشعة السينية والأشعة المقطعية وغيرها الكثير من التطبيقات. المنطق الترجيحي أو الضبابي Fuzzy Logic المنطق الضبابي هو تقنية حسابية تعتمد على استخدام الترجيح لاتخاذ القرار ويمكنه الاعتماد على معلومات إدخال غير دقيقة أو فيها بعض الأخطاء، وكلمة Fuzzy تعني أمر غير مؤكد أو غير محدد وهي تختلف عن المنطق الحاسوبي الذي يعطي نتيجة محددة إما 1 أي صواب أو 0 أي خطأ فهو يعطي إجابة قريبة من الحقيقة بدرجة معينة. على سبيل المثال إذا كان لديك سؤال تريد الإجابة عليه بالمنطق الضبابي فلن تحصل على إجابة أكيدة إما نعم أو لا وستكون الإجابة التي ستحصل عليها من من قبيل ربما نعم أو ربما لا أو بالتأكيد لا. يستخدم المنطق الضبابي في العديد من التطبيقات مثل ضبط حرارة مكيفات الهواء وفق درجة الحرارة المحيطة والتحكم في مياه الغسالات حسب حجم الملابس ودرجة اتساخها ونوع الأوساخ وما إلى ذلك. الأنظمة الخبيرة Expert systems الأنظمة الخبيرة هي تطبيقات حاسوبية مخصصة لحل المشكلات المعقدة في مجال معين بطريقة تحاكي الخبراء من البشر فهي أنظمة تحسن أداءها بمرور الوقت لأنها تكتسب المزيد من الخبرة تمامًا كما يفعل البشر لكنها تتميز عنا نحن البشر في كونها أكثر دقة في النتائج فهي لا ترتكب الأخطاء البشرية من سهو ونسيان ولا تطلق أحكامها بناء على العواطف بل على الحقائق فقط. تستخدم الأنظمة الخبيرة في أي مجال يحتاج لاستخدام الآلات في إطلاق الأحكام أو التنبؤات بدلًا من البشر مثل اكتشاف أعطال المركبات والتحكم في إشارات المرور واستنتاج أسباب الأمراض وتوقع عمليات الاحتيال والمعاملات المشبوهة …إلخ. وهناك بعض التخصصات الأخرى التي تتداخل مع الذكاء الصناعي إلى حد كبير جدًا، لكنها لا تعتبر فرعًا من فروع الذكاء الصناعي مثل: الرؤية الحاسوبية وعلم البيانات. وكنا قد تحدثنا عن هذه الفروع بشيء من التفصيل في مقالة مجالات الذكاء الاصطناعي فارجع إليها لمزيد من التفصيل. خريطة تعلم الذكاء الاصطناعي أحد أكبر الأسباب التي تجعل الناس يبتعدون عن مجال الذكاء الاصطناعي هو أنهم لا يعرفون من أين يبدؤون ولا يجدون المصادر الجيدة باللغة العربية كما أن هناك الكثير من المصطلحات الصعبة نسبيًا والتي تعترض طريقهم عندما يبحثون عن موارد لتعلم الذكاء الاصطناعي وتجعلهم يشعرون بالإحباط في البداية. عمومًا، يمكنك أن تتعلم المجال إما بدخول أروقة الجامعة وهو الطريق الأطول الذي يأخذ عدة سنوات ولا توفر أغلب الجامعات تعلم مجال الذكاء الاصطناعي من البداية بل يكون ضمن برامج الماجستير والدراسات العليا، عدا عن التركيز على الجانب النظري والتقنيات القديمة في ظل التسارع الرهيب لمجال علوم الحاسوب عمومًا والذكاء الاصطناعي خصوصًا، وقد فصلنا هذه النقطة في فقرة "طرق لتعلم البرمجة" من مقال كيف تتعلم البرمجة: نصائح وأدوات لرحلتك في عالم البرمجة أو يمكنك الاعتماد على الدورات البرمجية والمخيمات البرمجية والكتب المتخصصة لتعلم المجال منها خصوصًا، وهذا الأسلوب أقصر وأكثر عملية وأسرع للدخول في سوق العمل. سنعطيك الآن خارطة طريق ومصادر تعلم الذكاء الاصطناعي يمكنك من خلالها بدء رحلتك بسهولة لتعلم الذكاء الصناعي. هنا لا أقول أنه طريق سحري؛ بمجرد إنهائه تختم المجال، لا إطلاقًا، إنما هو طريق مناسب جدًا للدخول في هذا المجال. 1. تمكن من الأساسيات هذا يشمل مبادئ علوم الحاسب والرياضيات والاحتمالات والإحصاء. لقد أشرنا إلى الأساسيات في القسم السابق، فبالنسبة للرياضيات وما يتعلق بها، فغالبًا قد أنهيت الثانوية أو على وشك إنهائها ودرست بالفعل الرياضيات وهذا كافٍ للانطلاق. بالنسبة لتخصص علوم الحاسوب، تحتاج إلى فهم الحواسيب وعملها وأساسيات التعامل معها والتعامل مع أنظمة التشغيل وكيفية برمجتها وكتابة برامج لها …إلخ، وإن كنت مهتمًا بالحواسيب منذ صغرك وتعرف كيفية التعامل مع أنظمة التشغيل فيمكنك تخطي هذه الخطوة وبدء تعلم أساسيات لغة برمجة. يمكنك أيضًا البدء مع دورة علوم الحاسب التي يقدّمها نخبة من المطورين والمبرمجين في أكاديمية حسوب والذين يتابعون تقدمك في كل مرحلة ويجيبون على أي سؤال في حال واجهت مشكلة أو كان لديك استفسار أو طلبت شرحًا إضافيًّا، وتتضمن هذه الدورة كل ماتحتاجه لتصبح على معرفة واسعة بعلوم الحاسب، وهذا يتضمن: مكونات الحاسب، أساسيات البرمجة، لغات البرمجة، البرمجة الكائنية، الخوارزميات وهياكل البيانات، أنظمة التشغيل، قواعد البيانات، الويب، تصميم البرمجيات وغيرها. أما بالنسبة للإحصاء والاحتمالات فمعظمها يُفترض أن تكون تلقيته خلال دراستك الثانوية. عمومًا هناك الكثير من الدورات والمراجع المتوفرة على الإنترنت يمكنك الرجوع إليها. 2. تعلم أساسيات البرمجة ومفاهيمها ستحتاج إلى كتابة الشيفرات وبرمجة التطبيقات لذا وقبل البدء مباشرة بتعلم لغة البرمجة -والتي غالبًا ستكون بايثون- وتصعيب المهمة عليك لأنك ستتعلم لغة البرمجة ومفاهيم البرمجة في الوقت نفسه، ابدأ بتعلم مفاهيم البرمجة وأساسياتها أولًا والتي تجدها في كل لغات البرمجة وبذلك يسهل عليك تعلم استخدام أي لغة برمجة أخرى مهام كانت سهلة أو صعبة. أضع بين يديك مجموعة مقالات مناسبة بهذا الصدد يمكنك الرجوع لها: دليلك الشامل إلى تعلم البرمجة تعلم أساسيات البرمجة كيف تتعلم البرمجة: نصائح وأدوات لرحلتك في عالم البرمجة دليلك الشامل إلى أنواع البيانات تعرف على هياكل البيانات Data Structures 3. تعلم لغة بايثون لغة بايثون هي اللغة الأساسية والأقوى لتطوير تطبيقات تعلم الآلة والذكاء الصناعي، يمكنك بالطبع تعلم لغات برمجة أخرى مثل جافا Java و C++‎ إلا أن لغة بايثون تعد من أفضل لغات الذكاء الصناعي بسبب المزايا العديدة التي تقدمها مثل وفرة مكتباتها وسهولة كتابة تعليماتها البرمجية ودعم المجتمع والمرونة والاستقلالية والسرعة. يمكنك تعلّم لغة بايثون من خلال دورة تطوير التطبيقات باستخدام لغة Python الشاملة من أكاديمية حسوب التي تبدأ معك من الصفر حيث تعلمك أساسيات البرمجة وحتى احترافها بلغة بايثون، ثم تعلمك أساسيات الذكاء الاصطناعي وتعلم الآلة بإنشاء تطبيقات عملية تضيفها في معرض أعمالك، كما أن الدورة تضمن لك دخول سوق العمل بعد التخرج مباشرةً. 4. تعلم مفاهيم الذكاء الاصطناعي بالتزامن مع دورة بايثون سالفة الذكر، يمكنك البدء بقراءة كتاب مدخل إلى الذكاء الاصطناعي وتعلم الآلة وهو من أهم مصادر تعلم الذكاء الاصطناعي تتعرف من خلاله أكثر على أساسيات علم الذكاء الاصطناعي وتعلم الآلة وتتعرف على أهم مصطلحاته وتطبيقاته في حياتنا اليومية. ويمكنك الاطلاع على العديد من الدروس والمقالات المتنوعة الخاصة بالذكاء الصناعي في قسم الذكاء الصناعي في أكاديمية حسوب. 5. تعلم المكتبات وأطر العمل الأساسية توفر لك المكتبات وأطر العمل طرقًا مختصرة للعمل في مشاريع الذكاء الاصطناعي وهنا لن تحتاج إلى تعلم جميع أُطر ومكتبات الذكاء الصناعي بالطبع، فالأمر يعتمد على الفرع والمواضيع التي ترغب بالتخصص فيها. وإليك بعض هذه الأطر والمكتبات ومجال استخدامها: لبناء الشبكات العصبية: تنسرفلو TensorFlow وكيراس Keras وباي تورش PyTorch. غالبًا يكفي أن تتعلم واحدة منها، وكمبتدئ يكفي كيراس. لاستخدام خوارزميات تعلم الآلة: يمكنك استخدام مكتبة ساي كيت ليرن scikit-learn التي تتضمن تحقيقات لأهم خوارزميات تعلم الآلة، مثل خوارزمية التوقع الخطي وأشجار القرار. لتحليل وفهم البيانات وتوزيعها والعلاقات بينها: يمكنك الاعتماد على مكتبات بايثون، مثل: ماتبلوتليب Matplotlib وسيبورن Seaborn. 6. تعلم المعالجة المسبقة البيانات Data Preprocessing لا تتعامل تطبيقات وخوارزميات الذكاء الاصطناعي مع البيانات التي تستخرجها من الوسط المحيط مباشرة حيث تكون هذه البيانات بحالة فوضوية ومبعثرة، لذا تحتاج من تعلم تقنيات مختلفة تساعدك على تحويل هذه البيانات إلى شكل أفضل وأكثر فائدة من خلال تطبيق عمليات التنظيف والتوحيد والتشذيب عليها. تسمى هذه العمليات بالمعالجة المسبقة للبيانات وهي تحول البيانات الأولية إلى تنسيق مفهوم ومفيد وبجودة عالية وهي مهمة للغاية في مجال الذكاء الاصطناعي. 7. ابدأ بإنشاء المشاريع عندما تحاول إتقان الذكاء الاصطناعي، فإن النظرية وحدها لا تكفي والنهج العملي سيعزز تعلمك ويعزز مهاراتك لذا لابد لك من البدء بإنشاء مشاريع برمجية في الذكاء الصناعي لكي تصبح متمرّسًا في هذا المجال. هنا أنصحك بقراءة كتاب عشرة مشاريع عملية عن الذكاء الاصطناعي والذي يعد أيضًا من أهم مصادر تعلم الذكاء الاصطناعي العربية، ستجد فيه العديد من المشاريع الخاصة بالذكاء الصناعي، وهي مناسبة جدًا للمبتدئين. وكي تطور فهمك لخوارزميات الذكاء الاصطناعي أكثر يمكنك بناء هذه الخوارزميات من الصفر، ابدأ بالمشروعات التي تتطلب خوارزميات بسيطة ثم نفذ مشاريع أصعب، وزد مستوى المهارة المطلوبة تدريجيًا. على سبيل المثال يمكنك تطوير خوارزمية يمكنها التنبؤ بالحالة المزاجية لأصدقائك بحسب منشوراته في وسائل التواصل الاجتماعي أو حالته على الواتس وترسل لهم رسالة للاطمئنان عليهم أو تفكر في أي تطبيق آخر مفيد لمجتمعك. 8. تقدم بطلب للحصول على تدريب بعد الانتهاء من تلك عملية التعلم، سيكون الوقت قد حان للتقدّم للحصول على تدريب في شركة ما فالتدريب طريقة رائعة للحصول على بعض الخبرة الفعليّة وتسهيل البحث عن وظيفة مناسبة لاحقًا ولزيادة فرصك في الحصول على تدريب، يمكنك القيام بما يلي: أنشئ معرض أعمال يتضمن كافة مشاريع الذكاء الاصطناعي التي عملت عليها ووضح مساهمتك فيها. أخبر الأشخاص في شبكاتك المهنية والشخصية أنك تبحث عن تدريب (على لينكد إن مثلًا). احضر اللقاءات المحلية وهاكاثونات الذكاء الاصطناعي. حافظ على تحديث حساباتك في الشبكات المهنية. استعد للمقابلة دومًا وحضر إجاباتك على أكثر أسئلة مقابلات الذكاء الاصطناعي شيوعًا. 9. الحصول على عمل غالبًا يوفر التدريب الخبرة والاتصالات المهنية التي تساعدك في الحصول على وظيفة لذا عند الانتهاء من تدريبك، تواصل مع جهات الاتصال التي طورتها لإعلامهم بأنك تبحث عن وظيفة دائمة. الجانب الأكثر قيمة عند التدرب في الشركات هو منحك فرصة لحل مشاكل العالم الحقيقي. تأكد من إبراز مشاريع الذكاء الاصطناعي التي عملت عليها خلال فترة التدريب عندما تناقش ذلك مع أصحاب العمل المحتملين، بما في ذلك المساهمات التي قدمتها. 10. اسأل ولا تتردد حاول التعرف على من هم في نفس المجال سواء متعلمين أو خبيرين وتابعهم على وسائل التواصل أو ابحث عنهم في المجتمعات إذ سيشكلون طبقة الدعم والمساعدة لك في رحلتك في هذا المجال وكذلك ستفعل عندما تتعلم وتصبح خيبرًا وتمد يد العون للداخل الجديد وهكذا، ولا تتردد بطرح أي سؤال أو طلب أي مساعدة ولا تخجل إن كان سؤالك بسيطًا أو يبدو تافهًا. يمكنك أيضًا أن تضيف سؤالًا في قسم الأسئلة والأجوبة الذي توفره أكاديمية حسوب لتحصل على إجابة لسؤالك واستفسارك من مبرمجين آخرين ولتضمن لك الحصول على الدعم والمساعدة التي تحتاج إليها في بداية تعلمك. نصائح لتعلم الذكاء الاصطناعي بسرعة إن خيارات تعلم الذكاء الاصطناعي كثيرة متشعبة وكي لا تشعر بالتشتت والضياع إليك بعض النصائح التي تسرع عملية تعلمك وتمكنك من تحقيق نتائج ملموسة بزمن قصير: حدد من البداية هدفك من التعلم واختر مجال الذكاء الاصطناعي المناسب لك للتركيز عليه والتخصص به مما يعطيك فرص أكبر وأولوية للعمل به لاحقًا. اكتب كل ما تحتاج إلى تعلمه من الأساسيات التي ذكرناها في سياق المقال وضع خطة منهجية له وابدأ عملية التعلم والتزم بها قدر المستطاع. عزز ما تعلمته وطبقه على مشاريع عملية متدرجة في الصعوبة مستعينًا بالأدوات والأطر المناسبة التي تسرع عملك. حاول الحصول على تدريب عملي في إحدى الشركات أو المنظمات واعمل على مشاريع تحل مشاكل واقعية وتقدم قيمة فعلية. لا تستسلم إذا شعرت بالإحباط خلال إحدى مراحل التعلم فهذا أمر طبيعي، خذ فترة استراحة قصيرة وواصل ما بدأت به فالمجال واعد ومشرق ويستحق بذل الجهد. أسئلة شائعة حول الذكاء الاصطناعي 1. هل يمكنني تعلم الذكاء الاصطناعي بنفسي؟ نعم فبالرغم من كون الذكاء الاصطناعي مجالًا متقدمًا ويتضمن مفاهيم رياضية وبرمجية متقدمة لكن إذا كان لديك خطة واضحة للتعلم والتزمت بجدول زمني منتظم واتبعت كل النصائح التي وردت في سياق المقال فستتمكن بلا شك من تجاوز أي صعوبات وتعلمه بشكل ذاتي. 2. هل يمكن تعلم الذكاء الاصطناعي دون معرفة البرمجة؟ رغم توفر العديد من البيئات والأدوات المخصصة لحلول الذكاء الاصطناعي والتي لا تحتاج لكتابة شيفرات برمجية أو تتطلب كتابة القليل جدًا منها مثل Amazon SageMaker و DataRobot إلا أنه من الضروري أن تملك أساسًا في إحدى لغات البرمجة المختصة في الذكاء الاصطناعي مثل بايثون أو جافا أو C++‎ لتكون مهندس ذكاء اصطناعي محترف وتتمكن من حل أي مشكلة تواجهها بمرونة وكفاءة. 3. كم من الوقت يستغرق تعلم الذكاء الاصطناعي؟ يعتمد الجواب هنا على الخبرات الأساسية المسبقة المتاحة لديك ومدى قدرتك على التعلم لكن تعلم كافة أساسيات الذكاء الاصطناعي قد يحتاج منك حوالي ستة أشهر لعام تقريبًا بعدها يمكنك البدء في تنفيذ مشاريع بسيطة تناسب المبتدئين وتطور مستواك تدريجيًا. 5. ما هي مهارات تعلم الذكاء الاصطناعي؟ يجب أن يمتلك المتخصص في الذكاء الصناعي مجموعة من المهارات الشخصية إلى جانب مهاراته الفنية في البرمجة ومعرفته الجيدة في الرياضيات والإحصاء ولعل أبرزها التفكير النقدي والتحليلي والتواصل الفعال مع الآخرين والقدرة على اتخاذ القرارات. 4. أين أعثر على أفكار مشاريع في الذكاء الاصطناعي؟ إذا كنت ترغب في التدرب على مشاريع الذكاء الاصطناعي ولا تستطيع العثور على فكرة ملهمة فإليك بعض الاقتراحات لعشرة مشاكل يمكنك محاولة حلها باستخدام تقنيات الذكاء الاصطناعي. تصنيف التعليقات المزعجة أو المسيئة في المواقع اقتراح المنتجات الملائمة لاهتمامات العملاء التعرف على لغة الإشارة كشف الأخبار الكاذبة والمضللة تطبيق لتحليل السير الذاتية واختيار أفضل المرشحين كشف مشاهد العنف في الصور أو مقاطع الفيديو أداة التصحيح التلقائي للغة العربية التنبؤ بسعر المبيعات تطبيق التعرف على الوجه لفتح قفل الهاتف تطبيق للتنبؤ بالعمر وابحث وتابع صفحات ومنشورات ومواقع تتعلق بالمجال ومنها ستستلهم الكثير من الأفكار. 5. هل الذكاء الاصطناعي مطلوب في العالم العربي؟ نعم بكل تأكيد فالطلب على تخصص الذكاء الاصطناعي يزداد على مستوى العالم ككل وعلى مستوى الوطن العربي بشكل خاص وهو يدخل حيز التطبيق في كافة المجالات والقطاعات فإذا كان لديك شغف في تعلم الذكاء الاصطناعي أنصحك أن لا تضيع الوقت وتبدأ من الآن باستكشاف هذا المجال الثوري الذي سيكون سمة مميزة للعصر الحديث. خاتمة قدمنا في هذه المقالة دليلًا تفصيليًا سيساعدك على تعلم الذكاء الاصطناعي بالطريقة الحديثة. بدأنا بمقدمة أساسية للذكاء الاصطناعي وأشرنا إلى فروعه الواسعة. رأينا أيضًا المتطلبات الأساسية التي ستحتاجها قبل أن تبدأ بتعلم الذكاء الاصطناعي. ناقشنا خارطة طريق مفصلة يمكنك اعتمادها لتبدأ بسلاسة رحلة تعلم الذكاء الصناعي؛ بدأنا بأساسيات الذكاء الاصطناعي التي تضم جميع المفاهيم المهمة والموضوعات المطلوبة التي يجب أن تتعلمها لضمان رحلة سلسة. تحدثنا أيضًا عن أهم فروع الذكاء الصناعي والنصائح التي تساعدك على تعلمه بسرعة. لا شك أن الذكاء الاصطناعي مجال واسع ومربك بسبب المصطلحات التقنية الكثيرة التي تصادفها ونأمل أن يكون هذا الدليل البسيط قد ساعدك في تكوين فهم واضح عنه ومهد لك طريق التعلم وأجاب عن كافة تساؤلاتك حوله. اقرأ أيضًا برمجة الحاسوب للمبتدئين تعلم البرمجة مستقبل الذكاء الاصطناعي فوائد الذكاء الاصطناعي
  13. إن النمو السريع للذكاء الاصطناعي وتطبيقاته وقدراته القوية جعلت الناس يشعرون بالرهبة بشأن حتمية نجاح ثورة الذكاء الاصطناعي وقرب دخولها في كافة أشكال الحياة. كما أن التحول الذي أحدثه الذكاء الاصطناعي في الصناعات المختلفة جعل قادة الأعمال والعامة يعتقدون أننا على وشك تحقيق ذروة أبحاث الذكاء الصناعي والوصول إلى أعظم إمكانات الذكاء الصناعي، وهذا غير دقيق تمامًا. هنا يأتي دور معرفة وفهم أنواع الذكاء الاصطناعي الممكنة والأنواع الموجودة الآن على أرض الواقع؛ إن معرفة أنواع الذكاء الصناعي سيعطي صورة أوضح لقدرات الذكاء الصناعي الحالية والطريق الطويل الذي ينتظر أبحاث الذكاء الصناعي. إن أنواع الذكاء الصناعي هي تقسيمات لها طابع فلسفي أكثر من أي شيءٍ آخر، لذا عند الحديث عن أنواع الذكاء الاصطناعي، لابد من العودة بالزمن للخلف والنظر في المسائل المتعلقة بالذكاء الاصطناعي والتي حاول الفلاسفة حلها، مثل: كيف يعمل العقل؟ هل يمكن للآلات أن تتصرف بذكاء كما البشر؟ وإذا تصرفت كالبشر هل سيكون لديهم عقول حقيقية واعية أو مدركة؟ ما هي التداعيات الأخلاقية للآلات الذكية؟ وفقَا للفلاسفة، إن الإشارة إلى إمكانية تصرّف الآلات كما لو كانت ذكية يسمى فرضية الذكاء الاصطناعي الضعيفة Weak AI، أما الإشارة إلى أن تلك الآلات التي تتصرف بذكاء يمكنها التفكير أيضًا (وليس مجرد محاكاة التفكير) يسمى فرضية الذكاء الاصطناعي القوية Strong AI. يعتبر معظم الباحثين في مجال الذكاء الاصطناعي؛ أن فرضية الذكاء الاصطناعي الضعيفة أمرًا مفروغًا منه، ولا يهتمون بفرضية الذكاء الاصطناعي القوية، فطالما أن برنامجهم يعمل، فهم لا يهتمون بما إذا كنت تسميها محاكاة للذكاء أو ذكاء حقيقي. إن المصطلحان الذكاء الصناعي الضعيف والذكاء الاصطناعي القوي يمثلان أول نوعين ظهرا من الذكاء الصناعي، أما لاحقًا فقد ظهرت أنواع جديدة وفئات سنتعرّف عليها بالتفصيل في هذه المقالة. تجدر الإشارة بدايةً إلى أن هذه المقالة هي جزء من سلسلة مقالات متعلقة بالذكاء الاصطناعي، حيث بدأنا السلسلة بإلقاء نظرة موجزة شاملة على كل مايتعلق بهذا العلم في مقالة الذكاء الاصطناعي: دليلك الشامل وسنتحدث في هذا المقال بالتفصيل عن أنواع الذكاء الاصطناعي. أنواع الذكاء الاصطناعي نظرًا لأن أبحاث الذكاء الاصطناعي تهدف إلى جعل الآلات تحاكي الأداء البشري، فإن الدرجة التي يمكن لنظام الذكاء الاصطناعي أن يكرر بها القدرات البشرية تُستخدم كمعيار لتحديد أنواع الذكاء الاصطناعي. بالتالي اعتمادًا على كيفية مقارنة الآلة بالبشر من حيث التنوع والأداء، يمكن تصنيف الذكاء الاصطناعي إلى عدة أنواع الذكاء. في ظل هذا المبدأ، سيتم اعتبار الذكاء الاصطناعي الذي يمكنه أداء وظائف بشرية بمستويات متساوية من الكفاءة -كنوع أكثر تطورًا من الذكاء الاصطناعي، في حين أن الذكاء الاصطناعي الذي لديه وظائف وأداء محدود يعتبر نوعًا أبسط وأقل تطورًا. على وجه الدقة، فإن أنواع الذكاء الصناعي يمكن وضعها ضمن قسمين، الأول يعتمد على القدرات ومحاكاة التفكير البشري والثاني يعتمد على الوظيفية. أنواع الذكاء الاصطناعي وفقًا لقدراته أولى أنواع الذكاء الاصطناعي كانت مبنية على أساس القدرات، حيث قُسّم إلى ثلاث أنواع هي الذكاء الاصطناعي الضعيف والقوي والخارق. لنبدأ مع أول نوع من أنواع الذكاء الصناعي ضمن هذا القسم وهو الذكاء الصناعي الضعيف. ذكاء اصطناعي ضعيف Weak AI: هل تستطيع الآلات التصرف بذكاء؟ للإجابة على هذا السؤال سنرجع في الزمن إلى المقترح البحثي الذي أنتج أول تعريف للذكاء الصناعي والذي قدّمه جون ماكرثي وفريقه (McCarthy et al., 1955)، حيث أُكّد على أنه "يمكن وصف كل جانب من جوانب التعلم أو أي خاصيّة أخرى للذكاء بدقة بحيث يمكن صنع آلة لمحاكاته". بالتالي فإن الذكاء الاصطناعي تأسّس على افتراض أن الذكاء الاصطناعي الضعيف أمر ممكن. عمومًا أكّد البعض على أن الذكاء الاصطناعي الضعيف أمر مستحيل (Sayre, 1993)، لكن في العقد الأخير من الزمن أصبحنا نرى أنه ممكن. الآن وبعد أن أجبنا على السؤال الذي طرحناه في البداية (إمكانية التصرف بذكاء)؛ آن الأوان لنعطي تعريفًا يوضح الذكاء الاصطناعي الضعيف نفسه. لذا يمكننا القول بأن الذكاء الاصطناعي الضعيف هو الذكاء الصناعي الذي يتخصص في مجال واحد أو الذي يستطيع تنفيذ مهمة محددة فقط، فمثلاً هناك أنظمة ذكاء اصطناعي يمكنها التنبؤ بمرض محدد أو عدة أمراض لكن لا يمكنها توقع حالة الطقس. يُسمى هذا النوع أيضًا بالذكاء الاصطناعي الضيق Narrow AI أو الذكاء الاصطناعي المتخصص Specialized AI. عمومًا كلمة "ضعيف" تعني أن أنظمة الذكاء الاصطناعي هذه ليست قوية وغير قادرة على أداء مهام مفيدة، وهذا ليس هو الحال في وقتنا الحالي، فجميع التطبيقات الحالية للذكاء الاصطناعي تندرج تحت هذه الفئة والكثير منها يتفوق على البشر في المهام المحددة له (تسميتها بالضعيف أصبح غير دقيق). يمكننا توضيح الذكاء الصناعي الضعيف بالنقاط التالية: الذكاء الاصطناعي الأكثر شيوعًا من بين جميع أنواع الذكاء الصناعي الأخرى (والمتوفر حاليًا) هو الذكاء الاصطناعي الضعيف. لا يمكن للذكاء الصناعي الضعيف أن يتعدى مجاله أو حدوده، حيث يتم تدريبه على مهمة واحدة فقط (يفشل في أي مهمة أخرى). المساعد الشخصي الذكي سيري Siri هو مثال جيد عن هذا النوع، إضافةً إلى حاسوب واتسون Watson العملاق الخاص بشركة IBM أيضًا، حيث يستخدم نهج النظام الخبير جنبًا إلى جنب مع التعلم الآلي ومعالجة اللغة الطبيعية. من الأمثلة الأخرى على هذا النوع هي السيارات ذاتية القيادة، والتعرف على الكلام، والتعرف على الصور وتصنيف النصوص والترجمة الآلية …إلخ. دورة الذكاء الاصطناعي احترف برمجة الذكاء الاصطناعي AI وتحليل البيانات وتعلم كافة المعلومات التي تحتاجها لبناء نماذج ذكاء اصطناعي متخصصة. اشترك الآن ذكاء اصطناعي قوي Strong AI: هل يمكن للآلات أن تفكر؟ ثاني نوع ضمن هذا القسم من أنواع الذكاء الصناعي هو الذكاء الصناعي القوي. يمكن تعريف الذكاء الصناعي القوي (يُعرف أيضًا بالذكاء الصناعي العام General AI أو العميق Deep AI أو الكامل Full AI) على أنه الذكاء الصناعي الذي يمتلك قدرات عقلية وعمليات تفكير ووظائف مُكافئة للدماغ البشري. أي إنشاء آلات ذكية لا يمكن تمييزها عن العقل البشري. الفلاسفة هنا مهتمون بمشكلة مقارنة بنيتين: الإنسان والآلة، لذا طرحوا السؤال التالي "هل يمكن للآلات أن تفكر؟" هذا السؤال كان عليه العديد من الاعتراضات، فإمكانية تحقيق الذكاء الاصطناعي سيعتمد على كيفية تعريف الكلمة نفسها. قال عالم الحاسب Edsger Dijkstra إن "مسألة ما إذا كانت الآلات تستطيع التفكير، يتعلق بمسألة ما إذا كانت الغواصات تستطيع السباحة". أول تعريف للسباحة في قاموس American Heritage هو "التنقل عبر الماء عن طريق الأطراف أو الزعانف أو الذيل"، وبالتالي فإن الغواصات وفقًا لهذا التعريف لايمكنها السباحة. يعرّف القاموس أيضًا الطيران بأنه "التنقل في الهواء عن طريق الأجنحة"، إلا أن معظم الناس يتفق على أن الطائرات يمكنها الطيران والغواصات يمكنها السباحة. من هنا نستنتج أن الأسئلة والإجابات ليست فكرة دقيقة لتوضيح تصميم أو قدرات الطائرات والغواصات؛ نفس الأمر ينطبق على موضوعنا. لقد رفض آلان تورينج هذا السؤال "هل يمكن للآلات أن تفكر؟" واستبدله باختبار سلوكي يُعرف باختبار تورينج. إلا أن العديد من الفلاسفة ادعوا أن الآلة التي تجتاز اختبار تورينج لن تفكر في الواقع، لكنها ستكون مجرد محاكاة للتفكير. إلا أن تورينج عاد وقدم العديد من الحجج التي تدعم كلامه، لكن لن ندخل فيها الآن. من هنا نجد أن البشر لا يمكنهم حتى الاتفاق على ماهية الذكاء والتفكير (حتى اللحظة هذه على الأقل)، وبالتالي من الصعب جدًا إعطاء معيار واضح لما يمكن اعتباره نجاحًا في تطوير الذكاء الاصطناعي القوي. يتضح لنا مما سبق أيضًا أن الذكاء الاصطناعي القوي هو نهج فلسفي أكثر منه نهج عملي. يمكننا توضيح الذكاء الصناعي القوي بالنقاط التالية: مصطلح يشير إلى فكرة وجود آلة ذات ذكاء عام يمكنها التعلم وتطبيق ذكائها لحل كل مشكلة. يمكن للذكاء الاصطناعي القوي أن يفكر ويستوعب ويتصرف بطريقة مكافئة للبشر، كما يمكنه أداء أي مهمة فكرية بكفاءة مثل الإنسان. في الوقت الحالي، لا يوجد مثل هذا النظام. يركز الباحثون في جميع أنحاء العالم الآن على تطوير الآلات باستخدام الذكاء الاصطناعي القوي. ذكاء اصطناعي خارق Super AI أخيرًا هناك مايُسمّى بالذكاء الاصطناعي الخارق وهو النوع الثالث من أنواع الذكاء الاصطناعي. يصف هذا المصطلح سيناريو يتحسن فيه الذكاء الاصطناعي ذاتيًا بطريقة متسارعة ويتجاوز الذكاء البشري -بعبارةٍ أخرى، يصبح ذكاءً خارقًا. يمكننا توضيح الذكاء الصناعي الخارق بالنقاط التالية: الذكاء الصناعي الخارق هو مستوى الأنظمة الذكية حيث يمكن للآلات فيه أن تتفوق على الذكاء البشري، ويمكن أن تؤدي أي مهمة بطريقة أفضل من الإنسان ذي الخصائص المعرفية. وهو نتيجة للذكاء الاصطناعي العام General AI. يتضمن الذكاء الصناعي الخارق أمورًا مثل الذكاء الحقيقي والتفكير والإدراك والوعي وحل الألغاز وإصدار الأحكام والتخطيط والتعلم والتواصل. يرتبط الذكاء الصناعي الخارق بمفهوم التفرد التكنولوجي، الذي يفترض أن الآلات فائقة الذكاء سوف تتفوق على الحضارة البشرية. لا يزال مفهومًا افتراضيًا للذكاء الاصطناعي. أنواع الذكاء الاصطناعي وفقًا لوظيفته يمكن تصنيف الذكاء الصناعي أيضًا وفقًا للوظيفة Functionality التي يؤديها، فيما يلي سنعرض هذه الأنواع الأربعة. نضع في البداية تعريفًا بسيطًا ثم نوضحه. 1. الآلات التفاعلية Reactive machines يمثل هذا النوع أبسط أنواع الذكاء الصناعي وهي آلات تفاعلية بحتة ليس لديها ذاكرة ولاتُخزّن أيّة معلومات سابقة (تتفاعل وفقًا لمعطيات الموقف الحالي؛ لاتستخدم خبرتها السابقة). يُعتبر الحاسب العملاق Deep Blue من شركة IBM الذي يمتلك القدرة على لعب الشطرنج، والذي تغلب على البطل العالمي غاري كاسباروف في عام 1997، أفضل مثال لهذا النوع من الآلات. يستطيع الحاسب ديب بلو الذي يستخدم تقنيات الذكاء الصناعي -التعرف على القطع الموجودة على رقعة الشطرنج ومعرفة ودراسة كيفية تحرّك كل منها. يمكنه التنبؤ بأفضل الحركات التالية له ومايمكن لخصمه أن يفعل. إلا أن هذا الحاسب لا يتذكر أي شيء من الماضي، ولا أي ذكرى لما حدث من قبل خلال اللعبة (يتجاهل كل شيء قبل اللحظة الحالية). كل ما يفعله هو الاطلاع على القطع الموجودة على لوحة الشطرنج الآن، ثم اختيار أفضل حركة ممكنة. يُعد برنامج AlphaGo من جوجل مثالاً آخر على الأجهزة التفاعلية، واستطاع هزيمة أفضل اللاعبين. أسلوب التحليل الخاص به أكثر تعقيدًا من أسلوب Deep Blue، حيث يستخدم شبكة عصبية لتقييم تطورات اللعبة. تعمل هذه الآلات على تحسين قدرة أنظمة الذكاء الاصطناعي على لعب ألعاب معينة بطريقة أفضل، لكن لا يمكن تغييرها بسهولة أو تطبيقها على مواقف أخرى. هذه الآلات ليس لها فهم للعالم (تفهم أشياء محددة للغاية)- مما يعني أنها لا تستطيع العمل خارج المهام المحددة الموكلة إليها ويمكن خداعها بسهولة. يمكن أن يكون هذا جيدًا لضمان أن يكون نظام الذكاء الاصطناعي جديرًا بالثقة: فأنت تريد أن تكون سيارتك ذاتية القيادة موثوقًا بها، فربما من السيئ أن نرغب في أن تتفاعل الآلات حقًا مع العالم وفقًا لتخيلاتها المبنية على سيناريوهات سابقة. 2. الذاكرة المحدودة Limited memory تتمثل الخطوة التالية من خطوات تطور الذكاء الاصطناعي في تطوير القدرة على تخزين المعرفة. قال رافائيل تينا، كبير باحثي الذكاء الاصطناعي في شركة التأمين Acrisure Technology Group التي مقرها أوستن، إن الأمر سيستغرق ما يقرب ثلاثة عقود قبل أن يتم تحقيق هذا التطور. أحرز مجال الذكاء الاصطناعي في عام 2012 تقدمًا كبيرًا جدًا، حيث أتاحت الابتكارات الجديدة في جوجل و ImageNet للذكاء الاصطناعي إمكانية تخزين البيانات السابقة وإجراء التنبؤات باستخدامها. يشار إلى هذا النوع من الذكاء الاصطناعي على أنه ذكاء اصطناعي محدود الذاكرة، لأنه يمكنه بناء قاعدة معرفية محدودة خاصة به واستخدام تلك المعرفة للتحسين بمرور الوقت. تندرج جميع التطبيقات الحالية التي نعرفها تقريبًا ضمن هذه الفئة من الذكاء الاصطناعي. يتم تدريب جميع أنظمة الذكاء الاصطناعي الحالية من خلال كميات كبيرة من بيانات التدريب التي تخزنها في ذاكرتها لتشكيل نموذج مرجعي لحل المشكلات المستقبلية. تعد السيارات ذاتية القيادة من أفضل الأمثلة على أنظمة الذاكرة المحدودة. يمكن لهذه السيارات تخزين سرعة السيارات القريبة ومسافة السيارات الأخرى وحد السرعة ومعلومات أخرى للتنقل على الطريق. من الأمثلة الأخرى هي روبوتات المحادثة (ChatGPT مثلًا) والمساعدين الافتراضيين. ملاحظة: يمكن لأجهزة الذاكرة المحدودة تخزين التجارب السابقة أو بعض البيانات لفترة قصيرة من الوقت. 3. نظرية العقل Theory of mind في علم النفس، تُعتبر نظرية العقل أحد فروع العلوم الإدراكية وتتمثل بالقدرة على تمييز أو توقع ما سيفعله الشخص نفسه أو أشخاص آخرون وفهم أن للناس الآخرين معتقدات ونوايا ورغبات وآراء مختلفة. إذًا في هذا النوع يجب أن تتعلم الآلات الذكية المستقبلية كيفية فهم أن كل شخص (الأشخاص وكائنات الذكاء الاصطناعي) لديهم أفكار ومشاعر. يجب أن تعرف أنظمة الذكاء الصناعي المستقبلية كيفية تعديل سلوكها لتتمكن من السير بيننا. من حيث تقدم الذكاء الاصطناعي، فإن تقنية الذاكرة المحدودة هي أبعد ما وصلنا إليه -لكنها ليست الوجهة النهائية. يمكن لآلات الذاكرة المحدودة التعلم من التجارب السابقة وتخزين المعرفة، لكنها لا تستطيع التقاط التغييرات البيئية الدقيقة أو الإشارات العاطفية. مثلًا المساعدين الافتراضيين مثل أليكسا Alexa وسيري Siri لا تُبدي أي رد فعل عاطفي إذا صرخت عليهم". إن آلات الذاكرة المحدودة يمكن أن تنجز الكثير، لكن لا يمكنها الوصول إلى نفس مستوى الذكاء البشري حتى الآن. ربما يكون أداء السيارة ذاتية القيادة أفضل من أداء السائق البشري في معظم الأوقات لأنها لن ترتكب نفس الأخطاء البشرية. ولكن إذا كنت كسائق بشري تعلم أن ابن جارك يذهب إلى اللعب بالقرب من الشارع بعد المدرسة في يوم الخميس دومًا، فستعرف غريزيًا أنه يجب عليك أن تبطئ من سرعتك أثناء عبور ذلك الشارع -وهو شيء لن تكون عليه مركبة الذكاء الاصطناعي المزودة بذاكرة محدودة. يمكن أن تجلب نظرية العقل الكثير من التغييرات الإيجابية في عالم التكنولوجيا، لكنها أيضًا تنطوي على مخاطرها الخاصة. نظرًا لأن الإشارات العاطفية شديدة الدقة، فقد تستغرق أجهزة الذكاء الاصطناعي وقتًا طويلاً لتتقن قراءتها، ويمكن أن ترتكب أخطاء كبيرة أثناء مرحلة التعلم والتطبيق في المراحل الأولى. ملاحظة: لا يزال هذا النوع من آلات الذكاء الاصطناعي غير مطوّر، لكن الباحثين يبذلون الكثير من الجهود والتحسينات لتطوير مثل هذه الآلات. 4. الإدراك الذاتي Self-awareness تتمثل الخطوة الأخيرة في تطوير الذكاء الاصطناعي في بناء أنظمة يمكنها فهم نفسها وإدراك ذاتها. في آلات الإدراك الذاتي يتعين على باحثي الذكاء الاصطناعي بناء آلات تمتلك الوعي وليس فهم الوعي فقط. هذا النوع من أنواع الذكاء الاصطناعي هو امتداد لنظرية العقل التي ناقشناها منذ قليل. الإدراك يختلف عن إدراك الذات؛ الأول هو إدراك بيئة الفرد وجسده ونمط حياته، أما الثاني هو الاعتراف بهذا الإدراك. الإدراك الذاتي هو كيف يعرف الفرد ويفهم بوعي شخصيته ومشاعره ودوافعه ورغباته ("أريد هذا العنصر" يختلف تمامًا عن "أعلم أنني أريد هذا العنصر"). نفترض أن شخصًا ما يصرخ خلفنا أثناء الانتظار من أجل حركة المرور لأنه غاضب أو غير صبور، فهذا هو ما نشعر به عندما نصرخ على الآخرين. بدون نظرية العقل، لن نتمكن من إنجاز تلك الأنواع من الاستدلالات. في حين أننا ربما نكون بعيدين عن إنشاء آلات تدرك نفسها بنفسها، يجب أن نركز جهودنا نحو فهم الذاكرة والتعلم والقدرة على اتخاذ القرارات بناءً على التجارب السابقة. هذه خطوة مهمة لفهم الذكاء البشري تلقائيًا. ومن الأهمية بمكان إذا أردنا تصميم أو تطوير آلات تكون أكثر من استثنائية في تصنيف ما يرونه أمامهم. على الرغم من أن تطوير الإدراك الذاتي يمكن أن يعزز تقدمنا كحضارة على قدم وساق، إلا أنه يمكن أن يؤدي أيضًا إلى كارثة. لأنه بمجرد إدراكه لذاته، سيكون الذكاء الاصطناعي قادرًا على امتلاك أفكار مثل الحفاظ على الذات والتي قد تحدد بشكل مباشر أو غير مباشر النهاية للبشرية، حيث يمكن لمثل هذا الكيان أن يتفوق بسهولة على عقل أي إنسان ويخطط لمخططات غير متوقعة. خاتمة على الرغم من حقيقة أن الذكاء الصناعي لا يزال في بداياته وغير مستكشف تمامًا، إلا أنه ربما يكون أكثر إبداعات البشرية تعقيدًا وإذهالًا حتى الآن. مما يعني أن كل تطبيق ذكاء اصطناعي مذهل نراه اليوم يمثل مجرد قمة جبل الجليد للذكاء الاصطناعي. تحدثنا في هذه المقالة عن جميع فئات وأنواع الذكاء الاصطناعي، وهذه التصنيفات مهمة للباحثين والمطورين لتحديد أهدافهم التقنية. ومهما كان ما يخبئه المستقبل، فإن الذكاء الاصطناعي هو مجال بحثي ضخم ومهم وسريع النمو. على الرغم من أننا بالكاد خدشنا سطح إمكاناتها، فقد غيرت بالفعل الطريقة التي تؤدي بها كبرى الشركات عملها والذي سيتمد أيضًا لتغيير أسلوب حياة البشرية ككل. إن أردت الاستزادة، فإليك مصادر إضافية عربية لتعلم الذكاء الاصطناعي توفرها أكاديمية حسوب: البرمجة بلغة بايثون: كتاب يشرح لغة بايثون تمهيدًا لكتابة تطبيقات ذكاء اصطناعي وتعلم آلة بها. مدخل إلى الذكاء الاصطناعي وتعلم الآلة: كتاب يُعرِّفك على أساسيات الذكاء الاصطناعي وتعلم الآلة. عشرة مشاريع عملية عن الذكاء الاصطناعي: كتاب تطبق فيه ما تعلمته على مشاريع ذكاء اصطناعي عملية بلغة بايثون. قسم الذكاء الاصطناعي: يحوي مقالات متنوعة عن كل ما يتعلق بمجال الذكاء الاصطناعي. مجالات الذكاء الاصطناعي أهمية الذكاء الاصطناعي برمجة الذكاء الاصطناعي فوائد الذكاء الاصطناعي
  14. وسم البناء Build tag أو قيد البناء Build constraint هو مُعرّف يُضاف إلى التعليمات البرمجية لتحديد متى يجب تضمين ملف ما في حزمة أثناء عملية البناء build، ويتيح لك إمكانية بناء إصدارات مُختلفة لتطبيقك من نفس التعليمات البرمجية المصدرية والتبديل بينها بطريقة سريعة ومنظمة. يستخدم العديد من المطورين وسوم البناء لتحسين سير العمل Workflow عند بناء تطبيقات متوافقة مع جميع أنظمة تشغيل الأساسية Cross-platform، مثل البرامج التي تتطلب تغييرات في التعليمات البرمجية لمراعاة الفروقات بين أنظمة التشغيل المختلفة. تُستخدم وسوم البناء أيضًا من أجل اختبار التكامل Integration testing، مما يسمح لك بالتبديل بسرعة بين الشيفرة المتكاملة والشيفرة باستخدام خادم زائف Mock server أو شيفرة اختبارية بديلة Stub، وبين المستويات المختلفة لمجموعات الميزات التي يتضمنها تطبيقك. لنأخذ مثلًا، مشكلة اختلاف مجموعات الميزات التي تُمنح للعملاء، فعند كتابة بعض التطبيقات، قد ترغب في التحكم بالمميزات التي يجب تضمينها في الثنائي binary، مثل التطبيق الذي يوفّر مستويات مجانية واحترافية Pro ومتقدمة Enterprise. كلما رفع العميل من مستوى اشتراكه في هذه التطبيقات، توفّرت له المزيد من الميزات وأصبحت غير مقفلة. يمكنك حل هذه المشكلة من خلال الاحتفاظ بمشاريع منفصلة ومحاولة إبقائها متزامنةً مع بعضها بعضًا من خلال استخدام تعليمات الاستيراد import، وعلى الرغم من أن هذا النهج سيعمل، لكنه سيصبح مملًا بمرور الوقت وعرضةً للخطأ، وقد يكون النهج البديل هو استخدام وسوم البناء. ستستخدم في هذه المقالة وسوم البناء في لغة جو، لإنشاء ملفات تنفيذية مختلفة تُقدم مجموعات ميزات مجانية واحترافية ومتقدمة لتطبيقك. سيكون لكل من هذه الملفات التنفيذية مجموعةٌ مختلفةٌ من الميزات المتاحة، إضافةً للإصدار المجاني، الذي هو الخيار الافتراضي. ملاحظات: التوافق مع أنظمة التشغيل الأساسية Cross-platform: ‏ هو مصطلح يستخدم في علم الحوسبة يشير إلى برامج الحاسوب أو أنظمة التشغيل أو لغات الحاسوب أو لغات البرمجة وتطبيقاتها التي يمكنها العمل على عدة منصات حاسوبية. هناك نوعان رئيسيان من البرمجيات المتوافقة مع أنظمة التشغيل الأساسية، إذ يستلزم الأول بناءه لكل منصة يمكنه العمل عليها، والثاني يمكنه العمل مباشرةً على أي منصة تدعمه. اختبار التكامل Integration testing: يمثّل مرحلة اختبار البرامج التي تتكامل فيها الوحدات البرمجية وتُختبر مثل وحدة واحدة متكاملة. يُجرى اختبار التكامل لتقييم مدى امتثال نظام أو مكون برمجي لمتطلبات وظيفية محددة، وغالبًا ما تكون هذه المتطلبات مدونة في توثيق الخاصيات والمتطلبات. خادم زائف Mock server: هو إطار عمل يهدف إلى تبسيط اختبار التكامل. تعتمد هذه الأُطر على مفهوم الكائنات الزائفة، وهي كائنات محاكاة تحاكي سلوك الكائنات الحقيقية بطرق خاضعة للرقابة، وتكون غالبًا بمثابة جزء من عملية اختبار البرنامج. يُنشئ المبرمج عادةً كائنًا زائفًا لاختبار سلوك بعض الأشياء الأخرى، بنفس الطريقة التي يستخدم بها مصمم السيارة دمية اختبار التصادم لمحاكاة السلوك الديناميكي للإنسان في اصطدام السيارة. هذه التقنية قابلة للتطبيق أيضًا في البرمجة العامة. شيفرة اختبارية بديلة Stub: برنامج صغير يُستبدل ببرنامج أطول، ربما يُحمّل لاحقًا، أو يكون موجودًا عن بُعد في مكان ما، إذ يكون بديلًا مؤقتًا للشيفرة التي لم تُطوّر بعد، وهذه الشيفرة مفيدة جدًا في نقل البيانات والحوسبة الموزعة وكذلك تطوير البرمجيات واختبارها عمومًا. المتطلبات الأساسية أن يكون لديك مساحة عمل خاصة في لغة جو، وإذا لم يكن لديك مساحة عمل، اتبع سلسلة المقالات التالية: تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu، وتثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS، وتثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز. بناء النسخة المجانية سنبدأ ببناء الإصدار المجاني من التطبيق، إذ سيكون هو الإصدار الافتراضي عند تنفيذ الأمر go build دون أي وسوم بناء. سنستخدم لاحقًا وسوم البناء لإضافة أجزاء أخرى إلى برنامجنا. أنشئ مجلدًا باسم التطبيق الخاص بك في مجلد src، وسنستخدم هنا الاسم "app": $ mkdir app انتقل إلى المجلد "app" الذي أنشأته: $ cd app أنشئ الآن ملف "main.go" داخل مجلد المشروع. استخدمنا هنا محرر النصوص نانو nano لفتح وإنشاء الملف: $ nano main.go سنعرّف الآن الإصدار المجاني من التطبيق. انسخ المحتويات التالية إلى ملف main.go: package main import "fmt" var features = []string{ "Free Feature #1", "Free Feature #2", } func main() { for _, f := range features { fmt.Println(">", f) } } أنشأنا برنامجًا يُصرّح عن شريحة Slice باسم features، تحتوي على سلسلتين نصيتين strings تمثلان ميزات إصدار تطبيقنا المجاني. تستخدم الدالة ()main حلقة for لتنتقل عبر عناصر شريحة الميزات من أجل طباعة جميع الميزات المتاحة على الشاشة. احفظ الملف واخرج منه، وبعد حفظ الملف لن نضطر إلى تحريره مرةً أخرى خلال هذا المقال، إذ سنستخدم وسوم البناء لتغيير ميزات الثنائيات التي سنبنيها منها. اِبنِ وشغّل البرنامج: $ go build $ ./app ستحصل على الخرج التالي: > Free Feature #1 > Free Feature #2 طبع البرنامج ميزتين مجانيتين تكملان ميزات الإصدار المجاني من تطبيقنا. أكملنا الآن الإصدار المجاني المُتمثّل بتطبيق يحتوي على مجموعة ميزات أساسية جدًا. سنبني بعد ذلك شيفرة تمكننا من إضافة مزيدٍ من الميزات إلى التطبيق عند البناء. إضافة ميزات احترافية باستخدام go build تجنّبنا حتى الآن إجراء تغييرات على الملف main.go، وذلك لمحاكاة بيئة إنتاج عامة ينبغي فيها إضافة الشيفرة دون تغيير الشيفرة الرئيسية أو كسرها. ونظرًا لإمكانية تعديل ملف main.go، سنحتاج إلى استخدام آلية أخرى لإدخال المزيد من الميزات إلى شريحة features باستخدام وسوم البناء. سننشئ ملفًا جديدًا باسم "pro.go"، والذي سيستخدم الدالة ()init لإضافة المزيد من الميزات إلى شريحة features: $ nano pro.go أضف المحتويات التالية إلى الملف بعد فتحه: package main func init() { features = append(features, "Pro Feature #1", "Pro Feature #2", ) } استخدمنا الدالة ()init لتشغيل الشيفرة قبل الدالة ()main في التطبيق، ثم استخدمنا الدالة ()append لإضافة ميزات احترافية إلى شريحة features. احفظ الملف واخرج منه، ثم صرّف التطبيق وشغّله باستخدام الأمر التالي: $ go build نظرًا لوجود ملفين الآن في المجلد الحالي، هما "pro.go" و "main.go"، سينشئ الأمر go build ملفًا ثنائيًا من كليهما: $ ./app ستحصل على الخرج التالي: > Free Feature #1 > Free Feature #2 > Pro Feature #1 > Pro Feature #2 يتضمن التطبيق الآن كلًا من الميزات الاحترافية والمجانية، وهذا غير مرغوب بالوضع الحالي للتطبيق؛ فلا يوجد تمييز بين الإصدارات، إذ يتضمن الإصدار المجاني الميزات التي من المفترض أن تكون متوفرة فقط في الإصدار الاحترافي. يمكنك حل المشكلة عن طريق إضافة المزيد من التعليمات البرمجية لإدارة المستويات المختلفة للتطبيق، أو استخدام وسوم البناء لإخبار أدوات جو عن الملفات التي تكون بامتداد "go."،التي يجب بناؤها وتلك التي يجب تجاهلها. سنضيف في الخطوة التالية وسوم البناء. إضافة وسوم البناء يمكنك الآن استخدام وسوم البناء لتمييز الإصدار الاحترافي عن المجاني. يكون شكل الوسم كما يلي: // +build tag_name من خلال وضع هذا السطر البرمجي في بداية الحزمة الخاصة بك (في أول سطر) وتبديل tag_name إلى اسم وسم البناء الذي تريده، ستُوسّم هذه الحزمة لتصبح شيفرةً يمكن تضمينها اختياريًّا في الثنائي النهائي. دعنا نرى هذا عمليًا عن طريق إضافة وسم البناء إلى ملف pro.go لإخبار الأمر go build بتجاهلها ما لم يُحدّد الوسم. افتح الملف في محرر النصوص الخاص بك: $ nano pro.go ضِف مايلي: // +build pro package main func init() { features = append(features, "Pro Feature #1", "Pro Feature #2", ) } أضفنا في بداية الملف pro.go السطر ‎// +build proمتبوعًا بسطر جديد فارغ؛ وهذا السطر الجديد ضروري، وبدونه سيفسّر جو السطر السابق على أنه تعليق. يجب أن تكون تصريحات وسوم البناء أيضًا في أعلى الملف ذي الامتداد "go." دومًا، لا تضع أي شيء، ولا حتى التعليقات قبلها. يُخبِر التصريح build+ الأمر go build أن هذا ليس تعليقًا، بل هو وسم بناء. الجزء الثاني هو الوسم pro، فبإضافة هذا الوسم في الجزء العلوي من ملف pro.go، سيُضمّن الأمر go build ملف pro.go في حال وجود الوسم pro فقط. صرّف التطبيق الآن وشغّله: $ go build $ ./app ستحصل على الخرج التالي: > Free Feature #1 > Free Feature #2 بما أن ملف pro.go يتطلب وجود الوسم pro، سيجري تجاهل الملف وسيُصرّف التطبيق دونه. عند استخدام الأمر go build، يمكننا استخدام الراية tags- لتضمين شيفرة محددة لكي تُصرّف مع التطبيق عن طريق إضافة وسم الشيفرة مثل وسيط. سنجرّب ذلك مع الوسم pro: $ go build -tags pro ستحصل على الخرج التالي: > Free Feature #1 > Free Feature #2 > Pro Feature #1 > Pro Feature #2 سنحصل الآن على الميزات الاحترافية فقط إذا أضفنا الوسم pro. هذا جيد إذا كان هناك إصدارين فقط، ولكن الأمور تصبح معقدةً عند وجود مزيدٍ من الإصدارات وإضافة مزيدٍ من الوسوم. سنستخدم في الخطوة التالية وسوم بناء متعددة مع منطق بولياني Boolean logic، لإضافة إصدار مُتقدم. استخدام المنطق البولياني مع وسوم البناء عندما تكون هناك وسوم بناء متعددة في حزمة جو، تتفاعل الوسوم مع بعضها بعضًا باستخدام المنطق البولياني. لتوضيح ذلك، سنضيف المستوى "مُتقدم Enterprise" لتطبيقنا باستخدام الوسم pro والوسم enterprise. سنحتاج من أجل بناء ثنائي للإصدار المتقدم إلى تضمين كل من الميزات الافتراضية والميزات الاحترافية، ومجموعة جديدة من الميزات الخاصة بالإصدار المتقدم. افتح محررًا وأنشئ ملفًا جديدًا enterprise.go، لنضع فيه الميزات الجديدة: $ nano enterprise.go ستبدو محتويات enterprise.go متطابقة تقريبًا مع pro.go ولكنها ستحتوي على ميزات جديدة. ضِف الأسطر التالية إلى الملف "enterprise.go": package main func init() { features = append(features, "Enterprise Feature #1", "Enterprise Feature #2", ) } احفظ الملف واخرج. لا يحتوي ملف enterprise.go حاليًا على أي وسوم بناء، وكما تعلّمت عندما أضفت pro.go، فهذا يعني أن هذه الميزات ستضاف إلى الإصدار المجاني عند تنفيذ go.build. بالنسبة إلى pro.go، أضفت build pro+ \\ متبوعًا بسطر فارغ في أعلى الملف لإخبار الأمر go build أنه يجب تضمينه فقط عند استخدام tags pro-. تحتاج في هذه الحالة فقط إلى وسم بناء واحد لتحقيق الهدف، لكن عند إضافة الميزات الجديدة المتقدمة، إذ يجب أن يكون لديك أولًا ميزات احترافية Pro. سنجعل الآن ملف enterprise.go يدعم وسم البناء pro. افتح الملف باستخدام محرر النصوص الخاص بك: $ nano enterprise.go ضِف بعد ذلك وسم البناء قبل سطر التصريح package main وتأكد من إضافة سطر فارغ كما تحدثنا: // +build pro package main func init() { features = append(features, "Enterprise Feature #1", "Enterprise Feature #2", ) } احفظ الملف واغلقه، ثم صرّف التطبيق دون إضافة أي وسوم (الإصدار المجاني): $ go build $ ./app ستحصل على الخرج التالي: > Free Feature #1 > Free Feature #2 لاحظ أن الميزات المتقدمة لا تظهر في الإصدار المجاني. دعنا الآن نضيف وسم الإصدار المحترف pro ونبني التطبيق ونشغّله مرةً أخرى: $ go build -tags pro $ ./app ستحصل على الخرج التالي: > Free Feature #1 > Free Feature #2 > Enterprise Feature #1 > Enterprise Feature #2 > Pro Feature #1 > Pro Feature #2 ليس هذا فعلًا ما نحتاجه حتى الآن، لأن الميزات المتقدمة تظهر عندما نبني إصدار احترافي، وسنحتاج إلى استخدام وسم بناء آخر لحل هذه المشكلة. بالنسبة للوسم enterprise فهو على عكس الوسم pro، إذ نحتاج هنا إلى التأكد من توفُّر الميزات الاحترافية pro والمتقدمة enterprise في نفس الوقت. يراعي نظام البناء في جو هذا الموقف من خلال السماح باستخدام بعض المنطق البولياني في نظام وسوم البناء. لنفتح enterprise.go مرةً أخرى: $ nano enterprise.go سنضيف الآن وسمًا إلى هذا الملف باسم enterprise كما فعلنا سابقًا عند إضافة الوسم pro: // +build pro enterprise package main func init() { features = append(features, "Enterprise Feature #1", "Enterprise Feature #2", ) } احفظ الملف واخرج، ثم صرّف التطبيق مع إضافة الوسم enterprise: $ go build -tags enterprise $ ./app سيكون الخرج على النحو التالي: > Free Feature #1 > Free Feature #2 > Enterprise Feature #1 > Enterprise Feature #2 لاحظ أننا خسرنا ميزات الإصدار الاحترافي، وسبب ذلك هو أنه عندما نضع عدة وسوم بناء ضمن نفس السطر في ملف "go."، سيفسر go build أن العلاقة بينهما هي OR المنطقية. إذًا، بإضافة السطر build pro enterprise+ \\ سيُبنى الملف enterprise.go في حال وجود إحدى الوسمين pro أو enterprise. نحن الآن بحاجة إلى كتابة وسوم البناء بطريقة صحيحة مع استخدام المنطق AND، ولفعل ذلك سنكتب كلًا من الوسمين في سطر منفصل، وبهذا الشكل سيفسّر go build على أن العلاقة بينهما AND. افتح الملف enterprise.go مرةً أخرى وافصل وسوم البناء في أسطر منفصلة: // +build pro // +build enterprise package main func init() { features = append(features, "Enterprise Feature #1", "Enterprise Feature #2", ) } الآن، صرّف تطبيقك مع الوسم enterprise: $ go build -tags enterprise $ ./app ستحصل على الخرج التالي: > Free Feature #1 > Free Feature #2 ما زال هذا غير كافي لأن منطق AND يتطلب أن يكون العنصران محققين "true"؛ إذًا نحتاج إلى وجود وسمي البناء pro و enterprise: $ go build -tags "enterprise pro" $ ./app ستحصل على الخرج التالي: > Free Feature #1 > Free Feature #2 > Enterprise Feature #1 > Enterprise Feature #2 > Pro Feature #1 > Pro Feature #2 يمكنك الآن بناء التطبيق من نفس شجرة المصدر بعدة طرق مختلفة وفقًا للميزات التي تريد توفيرها. استخدمنا في هذا المثال وسم بناء جديد باستخدام وسم build+ // للدلالة على منطق AND، ولكن هناك طرق بديلة لتمثيل المنطق البولياني باستخدام وسوم البناء. يحتوي الجدول التالي على بعض الأمثلة على التنسيقات النحوية الأخرى لوسوم البناء، جنبًا إلى جنب مع مكافئها المنطقي: قاعدة وسم البناء عينة عن الوسم التعليمة المنطقية عناصر مفصولة بفراغ build pro enterprise+ \\ محترف أو متقدم pro OR enterprise -------- -------- -------- عناصر مفصولة بفاصلة build pro,enterprise+ \\ محترف ومتقدم pro AND enterprise -------- -------- -------- عناصر مفصولة بعلامة تعجب build !pro+ \\ ليس محترف NOT pro خاتمة تعرّفت في هذا المقال على كيفية استخدام وسوم البناء للتحكم في الملفات التي ستُصرّف إلى ملفات ثنائية في جو؛ إذ صرّحنا أولًا عن وسم البناء، ثم استخدمناها في go build ثم دمجنا عدة وسوم باستخدام المنطق البولياني. أخيرًا بنينا برنامجًا يتضمن مجموعات ميزات مختلفة كل منها يمثل إصدارًا (مجاني، احترافي، متقدم)، والذي أظهر قوة وسوم البناء التي تمثّل أداةً قوية جدًا بالتحكم بإصدارات المشروع. ترجمة -وبتصرف- للمقال Customizing Go Binaries with Build Tags لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: تعرف على دالة التهيئة init واستخدامها في لغة جو Go العمليات الحسابية في لغة جو Go مدخل إلى البيانات وأنواعها: أنواع البيانات الأساسية
  15. تُستخدَم الدالة المُعرّفة مسبقًا ()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. اقرأ أيضًا المقال السابق: التعليمة defer في لغة جو Go كيفية تعريف واستدعاء الدوال في لغة جو Go
  16. تتضمن لغة جو العديد من تعليمات التحكم بسير عمل البرنامج مثل if و switch و for …إلخ، وهذه التعليمات موجودة في أغلب لغات البرمجة، إلا أنّ هناك تعليمة خاصة في لغة جو غير موجودة في معظم اللغات الأخرى هي تعليمة التأجيل defer. على الرغم من أن هذه التعليمة ليست مشهورة إلا أنها مفيدة، فالهدف الأساسي من هذه التعليمة هو إجراء عملية تنظيف الموارد بطريقة سليمة بعد انتهاء استخدامها مثل الملفات المفتوحة أو الاتصالات بالشبكة أو مقابض قاعدة البيانات handles بعد تأجيلها ريث الانتهاء منها، إذ تُعَدّ عملية تنظيف أو تحرير الموارد بعد استخدامها أمرًا مهمًا جدًا للسماح للمستخدِمين الآخرين باستخدام هذا المورد دون وجود أي بقايا تركها المستخدِم السابق سواءً كان المستخدِم آلةً أو شخصًا أو برنامجًا أو جزءًا آخر من الشيفرة نفسها. تساعدنا تعليمة التأجيل defer في جعل الشيفرة أنظف وأكثر متانةً وأقل عرضةً للأخطاء من خلال جعل تعليمة استدعاء أو حجز الملف أو المورد قريبة من تعليمة تحريره، وفي هذا المقال سنتعلم كيفية استخدام تعليمة التأجيل defer بطريقة صحيحة لتنظيف الموارد بالإضافة إلى العديد من الأخطاء الشائعة التي تحدث عند استخدام هذه التعليمة. ما هي تعليمة defer؟ تسمح لك جو بتأجيل تنفيذ استدعاء دالة بإضافتها إلى طابور تنفيذ خاص ريثما تكون الدالة المُستدعاة ضمنها قد أكتمل تنفيذها من خلال استخدام الكلمة المفتاحية defer قبلها بالشكل التالي: package main import "fmt" func main() { defer fmt.Println("Bye") fmt.Println("Hi") } أجّلنا هنا استخدام الدالة ("fmt.Println("Bye إلى حين انتهاء تنفيذ الدالة المُستدعاة ضمنها أي الدالة main، وبالتالي سيكون الخرج كما يلي: Hi Bye أي سيُنفّذ هنا كل شيء داخل الدالة main ثم بعد الانتهاء من كل التعليمات (هنا لا توجد إلا تعليمة واحدة هي التعليمة التي تطبع Hi) ستُنفَّذ التعليمة المؤجلة، كما رأيت بالمثال. والآن قد يتبادر إلى الأذهان السؤال التالي: ماذا لو كان هناك أكثر من استدعاء مؤجل، أي أكثر من دالة مؤجلة، ماذا سيحدث؟ كل دالة تُسبق بتعليمة التأجيل تُضاف إلى مكدس، ومن المعروف أنّ المكدس هو بنية معطيات تخرج منها البيانات وفق مبدأ مَن يدخل آخرًا يخرج أولًا، وبالتالي إذا كان لدينا أكثر من استدعاء مؤجل، فسيكون التنفيذ وفق هذه القاعدة، ولجعل الأمور أبسط سنأخذ المثال التالي: package main import "fmt" func main() { defer fmt.Println("Bye1") defer fmt.Println("Bye2") fmt.Println("Hi") } سيكون الخرج كما يلي: Hi Bye2 Bye1 لدينا في البرنامج السابق استدعاءان مؤجلان، إذ يُضاف أولًا إلى المكدس التعليمة التي تطبع Bye1 ثم التعليمة التي تطبع Bye2، ووفق القاعدة السابقة ستُطبع التعليمة التي أٌضيفت أخيرًا إلى المكدس أي Bye2 ثم Bye1، وبالطبع تُنفَّذ التعليمة التي تطبع Hi وأيّ تعليمة أخرى ضمن الدالة main قبل تنفيذ أيّ تعليمة مؤجلة، ولدينا مثال آخر كما يلي: package main import "fmt" func main() { fmt.Println("Hi0") defer fmt.Println("Bye1") defer fmt.Println("Bye2") fmt.Println("Hi1") fmt.Println("Hi2") } سيكون الخرج كما يلي: Hi0 Hi1 Hi2 Bye2 Bye1 عند تأجيل تنفيذ دالة ما ستقيَّم الوسائط على الفور، وبالتالي لن يؤثر أيّ تعديل لاحق، لذا انتبه جيدًا إلى المثال التالي: package main import "fmt" func main() { x := 9 defer fmt.Println(x) x = 10 } سيكون الخرج كما يلي: 9 كانت هنا قيمة x تساوي 9 ثم أجلنا تنفيذ دالة الطباعة التي تطبع قيمة هذا المتغير، وعلى الرغم من أنّ تعليمة x=10 ستُنفَّذ قبل تعليمة الطباعة، إلا أنّ القيمة التي طُبعَت هي القيمة السابقة للمتغير x وذلك للسبب الذي ذكرناه منذ قليل. ما تعلمناه حتى الآن كان بغرض التوضيح فقط، إذ لا تُستخدم بهذا الشكل بل عادة ما تُستخدم تعليمة التأجيل في تنظيف الموارد وهذا ما سنراه تاليًا. تنظيف الموارد باستخدام تعليمة التأجيل يُعَدّ استخدام تعليمة التأجيل لتنظيف الموارد أمرًا شائعًا جدًا في جو، وسنلقي الآن نظرةً على برنامج يكتب سلسلةً نصيةً في ملف ولكنه لا يستخدِم تعليمة التأجيل لتنظيف المورد: package main import ( "io" "log" "os" ) func main() { if err := write("readme.txt", "This is a readme file"); err != nil { log.Fatal("failed to write file:", err) } } func write(fileName string, text string) error { file, err := os.Create(fileName) if err != nil { return err } _, err = io.WriteString(file, text) if err != nil { return err } file.Close() return nil } لدينا في هذا البرنامج دالة تسمى write والهدف منها بدايةً هو محاولة إنشاء ملف، وإذا حصل خطأ أثناء هذه المحاولة، فستُعيد هذا الخطأ وتُنهي التنفيذ؛ أما في حال نجاح عملية إنشاء الملف، فستكتب فيه السلسلة This is a readme file، وفي حال فشلت عملية الكتابة، فستُعيد الخطأ وتنهي تنفيذ الدالة أيضًا، وأخيرًا إذا نجح كل شيء، فستغلق الدالة الملف الذي أُنشِئ معيدة إياه إلى نظام الملفات ثم تُعيد القيمة nil إشارةً إلى نجاح التنفيذ. هذا البرنامج يعمل بطريقة سليمة، إلا أنّ هناك زلة برمجية صغيرة؛ فإذا فشلت عملية الكتابة في الملف، فسيبقى الملف المُنشئ مفتوحًا، أي توجد هناك موارد غير محررة، ولحل هذه المشكلة مبدأيًا يمكنك تعليمة file.Close()‎ أخرى ومن دون استخدام تعليمة التأجيل: package main import ( "io" "log" "os" ) func main() { if err := write("readme.txt", "This is a readme file"); err != nil { log.Fatal("failed to write file:", err) } } func write(fileName string, text string) error { file, err := os.Create(fileName) if err != nil { return err } _, err = io.WriteString(file, text) if err != nil { file.Close() return err } file.Close() return nil } الآن سيُغلق الملف حتى إذا فشل تنفيذ io.WriteString، وقد يكون هذا حلًا بسيطًا وأسهل من غيره، لكن إذا كانت الدوال أكثر تعقيدًا، فقد ننسى إضافة هكذا تعليمات في الأماكن المُحتملة التي قد ينتهي فيها التنفيذ قبل تحرير المورد، لذا يتمثّل الحال الأكثر احترافيةً وأمانًا باستخدام تعليمة التأجيل مع الدالة file.Close وبالتالي ضمان تنفيذها دومًا بغض النظر عن أيّ مسار تنفيذ أو خطأ مُفاجئ قد يؤدي إلى إنهاء الدالة: package main import ( "io" "log" "os" ) func main() { if err := write("readme.txt", "This is a readme file"); err != nil { log.Fatal("failed to write file:", err) } } func write(fileName string, text string) error { file, err := os.Create(fileName) if err != nil { return err } defer file.Close() _, err = io.WriteString(file, text) if err != nil { return err } return nil } أضفنا هنا السطر defer file.Close لإخبار المُصرّف بإغلاق الملف بعد انتهاء تنفيذ الدالة، وبالتالي ضمان تحريره مهما حدث. انتهينا هنا من زلة برمجية ولكن ستظهر لنا زلة برمجية أخرى؛ ففي حال حدث خطأ في عملية إغلاق الملف file.Close، فلن تكون هناك أيّ معلومات حول ذلك الخطأ، أي لن يُعاد الخطأ، وبالتالي يمنعنا استخدام تعليمة التأجيل من الحصول على الخطأ أو الحصول على أيّ قيمة تُعيدها الدالة المؤجلة. يُعَدّ استدعاء الدالة Close في جو أكثر من مرة آمنًا ولا يؤثر على سلوك البرنامج، وفي حال كان هناك خطأ، فسيُعاد من المرة الأولى التي تُستدعى فيها الدالة، وبالتالي سيسمح لنا هذا باستدعائها ضمن مسار التنفيذ الناجح في الدالة. سنعالج في المثال التالي المشكلة السابقة التي تتجلى بعدم إمكانية الحصول على معلومات الخطأ في حال حدوثه مع تعليمة الإغلاق المؤجلة: package main import ( "io" "log" "os" ) func main() { if err := write("readme.txt", "This is a readme file"); err != nil { log.Fatal("failed to write file:", err) } } func write(fileName string, text string) error { file, err := os.Create(fileName) if err != nil { return err } defer file.Close() _, err = io.WriteString(file, text) if err != nil { return err } return file.Close() } لاحظ أنّ التعديل الوحيد على الشيفرة السابقة كان بإضافة السطر return file.Close()‎، وبالتالي إذا حصل خطأ في عملية الإغلاق، فسيُعاد مباشرةً إلى الدالة التي تستدعي الدالة write، ولاحظ أيضًا أنّ التعليمة defer file.Close ستُنفّذ بكافة الأحوال، مما يعني أنّ تعليمة الإغلاق ستُنفّذ مرتين غالبًا، وعلى الرغم من أنّ ذلك ليس مثاليًّا، إلا أنه مقبول وآمن. إذا ظهر خطأ في تعليمة WriteString، فسيُعاد الخطأ وينتهي تنفيذ الدالة ثم ستُنفّذ تعليمة file.Close لأنها تعليمة مؤجلة، وهنا على الرغم من إمكانية ظهور خطأ عند محاولة إغلاق الملف، إلا أنّ هذا الخطأ لم يَعُد مهمًا لأن الخطأ الذي تعيده WriteString سيكون غالبًا هو السبب وراء حدوثه. تعرّفنا حتى الآن على كيفية استخدام تعليمة التأجيل واحدة لضمان تحرير أو تنظيف مورد ما، وسنتعلّم الآن كيفية استخدام عدة تعليمات تأجيل من أجل تنظيف أكثر من مورد. استخدام تعليمات تأجيل متعددة من الطبيعي أن تكون لديك أكثر من تعليمة defer في دالة ما، لذا دعنا ننشئ برنامجًا يحتوي على تعليمات تأجيل فقط لنرى ما سيحدث في هذه الحالة (كما أشرنا سابقًا): package main import "fmt" func main() { defer fmt.Println("one") defer fmt.Println("two") defer fmt.Println("three") } سيكون الخرج كما يلي: three two one كما ذكرنا سابقًا أنّ سبب الطباعة بهذا الترتيب هو أن تعليمة التأجيل تعتمد على تخزين سلسلة الاستدعاءات ضمن بنية المكدس، والمهم الآن أن تعرف أنه يمكن أن يكون لديك العديد من الاستدعاءات المؤجلة حسب الحاجة في دالة ما، ومن المهم أن تتذكر أنها تُستدعى جميعها بعكس ترتيبها في الشيفرة حسب بنية المكدس. الآن بعد أن فهمنا هذه الفكرة جيدًا، سننشئ برنامجًا يفتح ملفًا ويكتب عليه ثم يفتحه مرةً أخرى لنسخ المحتويات إلى ملف آخر: package main import ( "fmt" "io" "log" "os" ) func main() { if err := write("sample.txt", "This file contains some sample text."); err != nil { log.Fatal("failed to create file") } if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil { log.Fatal("failed to copy file: %s") } } func write(fileName string, text string) error { file, err := os.Create(fileName) if err != nil { return err } defer file.Close() _, err = io.WriteString(file, text) if err != nil { return err } return file.Close() } func fileCopy(source string, destination string) error { src, err := os.Open(source) if err != nil { return err } defer src.Close() dst, err := os.Create(destination) if err != nil { return err } defer dst.Close() n, err := io.Copy(dst, src) if err != nil { return err } fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination) if err := src.Close(); err != nil { return err } return dst.Close() } لدينا هنا دالة جديدة fileCopy تفتح أولًا ملف المصدر الذي سننسخ منه ثم تتحقق من حدوث خطأ أثناء فتح الملف، فإذا كان الأمر كذلك، فسنعيد الخطأ ونخرج من الدالة، وإلا فإننا نؤجل إغلاق الملف المصدر الذي فتحناه. نُنشئ بعد ذلك ملف الوجهة ونتحقق من حدوث خطأ أثناء إنشاء الملف، فإذا كان الأمر كذلك، فسنعيد هذا الخطأ ونخرج من الدالة، وإلا فسنؤجل أيضًا إغلاق ملف الوجهة، وبالتالي لدينا الآن دالتان مؤجلتان ستُستدعيان عندما تخرج الدالة من نطاقها. الآن بعد فتح كل من ملف المصدر والوجهة فإننا سننسخ ()Copy البيانات من الملف المصدر إلى الملف الوجهة، فإذا نجح ذلك، فسنحاول إغلاق كلا الملفين، وإذا تلقينا خطأً أثناء محاولة إغلاق أيّ ملف، فسنُعيد الخطأ ونخرج من الدالة. لاحظ أننا نستدعي ()Close لكل ملف صراحةً على الرغم من أنّ تعليمة التأجيل ستستدعيها أيضًا، والسبب وراء ذلك هو كما ذكرناه في الفقرة السابقة؛ وهو للتأكد من أنه إذا كان هناك خطأ في إغلاق ملف، فإننا نُعيد معلومات هذا الخطأ، كما يضمن أنه إذا أُنهيَت الدالة مُبكرًا لأيّ سبب من الأسباب بسبب خطأ ما مثل الفشل في عملية النسخ بين الملفين، فسيحاول كل ملف الإغلاق بطريقة سليمة من خلال تنفيذ الاستدعاءات المؤجلة. الخاتمة تحدثنا في هذا المقال عن تعليمة التأجيل defer وكيفية استخدامها والتعامل معها وأهميتها في تنظيف وتحرير الموارد بعد الانتهاء من استخدامها، وبالتالي ضمان توفير الموارد للمستخدِمين المختلفِين وتوفير الذاكرة. يمكنك أيضًا الرجوع إلى مقال معالجة حالات الانهيار في لغة جو Go: يتضمن حالةً خاصةً من حالات استخدام تعليمة التأجيل. ترجمة -وبتصرُّف- للمقال Understanding defer in Go لصاحبه Gopher Guides. اقرأ أيضا المقال السابق: كيفية تعريف واستدعاء الدوال في لغة جو Go كيفية كتابة التعليمات الشرطية if في لغة جو Go فهم الملفات Files وأنظمة الملفات file systems
  17. تُعَدّ الدالة funtion كتلةً من التعليمات التي تنفِّذ إجراءً ما، ويمكن بعد تعريفها إعادة استخدامها في أكثر من موضع. تجعل الدوال الشيفرة تركيبية modular، مما يسمح بتقسيم الشيفرة إلى أجزاء صغيرة سهلة الفهم واستخدامها مرارًا وتكرارًا. تضم لغة جو مكتبةً قياسيةً تدعى fmt، إذ تحتوي على عدد من الدوال المُضمّنة التي قد تكون شائعة بالنسبة لك مثل: الدالة ()fmt.Println تُستخدَم للطباعة على شاشة الخرج القياسية المُستخدَمة. الدالة ()fmt.Printf تُستخدَم للطباعة مع إمكانية تنسيق الخرج. تتضمن أسماء الدوال الأقواس وقد تتضمن معامِلات أيضًا، وسنتعلم في هذا المقال كيفية تعريف الدوال وكيفية استخدامها في البرامج. تعريف الدالة لنبدأ بتحويل برنامج "Hello, World!‎" إلى دالة، لذا أنشئ ملفًا نصيًا جديدًا وافتحه في محرر النصوص المفضل عندك ثم استدع البرنامج ‎hello.go. تُعرَّف الدالة باستخدام الكلمة المفتاحية func متبوعة باسم من اختيارك ثم قوسين يمكن أن يَحتويا المعامِلات التي ستأخذها الدالة ثم ينتهي التعريف بنقطتين، وسنعرّف هنا دالةً باسم ‎‎hello()‎‎: func hello() {} أعددنا في الشفرة أعلاه تعليمة التهيئة لإنشاء الدالة، وبعد ذلك سنضيف سطرًا ثانيًا مُزاحًا بأربع مسافات بيضاء ثم سنكتب التعليمات التي ستنفّذها الدالة، وفي هذه الحالة سنطبع العبارة !Hello, World في الطرفية: func hello() { fmt.Println("Hello, World!") } أتممنا تعريف دالتنا ولكن إذا نَفَّذنا البرنامج الآن، فلن يحدث أيّ شيء لأننا لم نستدع الدالة، لذلك سنستدعي الدالة ‎hello()‎ ضمن الدالة ()main كما يلي: package main import "fmt" func main() { hello() } func hello() { fmt.Println("Hello, World!") } لننفّذ البرنامج الآن كما يلي: $ go run hello.go يجب أن تحصل على الخرج التالي: Hello, World! الدالة ()main هي دالة خاصة تخبر المُصرّف أنّ هذا هو المكان الذي يجب أن يبدأ منه البرنامج، فأيّ برنامج تريده أن يكون قابلاً للتنفيذ (برنامج يمكن تشغيله من الطرفية)، فستحتاج إلى دالة ()main، كما يجب أن تظهر الدالة ()main مرةً واحدةً فقط وأن تكون في الحزمة main ولا تستقبل أو تعيد أيّ وسائط، وهذا يسمح بتنفيذ البرنامج في أيّ برنامج جو آخر حسب المثال التالي: package main import "fmt" func main() { fmt.Println("this is the main section of the program") } بعض الدوال أكثر تعقيدًا بكثير من الدالة ‎hello()‎ التي عرّفناها أعلاه، إذ يمكننا على سبيل المثال استخدام حلقة for والتعليمات الشرطية وغيرها داخل كتلة الدالة، فالدالة المُعرّفة أدناه على سبيل المثال تستخدِم تعليمةً شرطيةً للتحقق مما إذا كانت المدخلات الممرّرة إلى المتغير ‎name‎ تحتوي على حرف صوتي vowel، ثم تستخدِم الحلقة ‎for‎ للتكرار على الحروف الموجودة في السلسلة النصية ‎name‎. package main import ( "fmt" "strings" ) func main() { names() } func names() { fmt.Println("Enter your name:") var name string fmt.Scanln(&name) // Check whether name has a vowel for _, v := range strings.ToLower(name) { if v == 'a' || v == 'e' || v == 'i' || v == 'o' || v == 'u' { fmt.Println("Your name contains a vowel.") return } } fmt.Println("Your name does not contain a vowel.") } تستخدِم الدالة ‎names()‎ التي عرّفناها أعلاه تعليمةً شرطيةً وحلقة ‎for‎، وهذا توضيح لكيفية تنظيم الشيفرة البرمجية ضمن تعريف الدالة، كما يمكننا أيضًا جعل التعليمة الشرطية والحلقة ‎for‎ دالتين منفصلتين. يجعل تعريف الدوال داخل البرامج الشفرة البرمجية تركيبيةً modular وقابلةً لإعادة الاستخدام، وذلك سيتيح لنا استدعاء الدالة نفسها دون إعادة كتابة شيفرتها في كل مرة. المعاملات عرّفنا حتى الآن دالة ذات قوسين فارغين لا تأخذ أيّ وسائط arguments، وسنتعلم في هذا القسم كيفية تعريف المعامِلات parameters وتمرير البيانات إلى الدوال. يُعَدّ المعامِل كيانًا مُسمًّى يوضَع في تعريف الدالة ويعرِّف وسيطًا يمكن أن تقبله الدالة عند استدعائها، ويجب عليك في لغة Go أن تحدد نوع البيانات data type لكل معامِل لننشئ برنامجًا يُكرر كلمة عدة مرات، إذ سنحتاج إلى متغير من النوع string سنسميه word ومتغير من النوع int سنسميه reps يُحدد عدد التكرارات. package main import "fmt" func main() { repeat("Sammy", 5) } func repeat(word string, reps int) { for i := 0; i < reps; i++ { fmt.Print(word) } } مرّرنا السلسلة Sammy إلى المتغير word والقيمة 5 إلى المعامِل reps وذلك وفقًا لترتيب المعامِلات في ترويسة الدالة repeat، إذ تتضمّن الدالة حلقة for تطبع قيمة المعامِل word عدة مرات يُحدّدها المعامِل reps، وسيكون الخرج كما يلي: SammySammySammySammySammy إذا كانت لديك مجموعة من المعامِلات وجميعها تمتلك القيمة نفسها، فيمكنك تجاهل تحديد النوع من أجل كل متغير كما سنرى، لذا دعنا ننشئ برنامجًا صغيرًا يأخذ ثلاثة معامِلات ‎x‎ و ‎y‎ و ‎z‎ من النوع int، إذ سننشئ دالةً تجمع تلك المعامِلات وفق عدة مجموعات ثم تطبع الدالة حاصل جمعها. package main import "fmt" func main() { addNumbers(1, 2, 3) } func addNumbers(x, y, z int) { a := x + y b := x + z c := y + z fmt.Println(a, b, c) } عند تعريف الدالة addNumbers لم نكن بحاجة إلى تحديد نوع كل متغير على حدة، وإنما وضعنا نوع بيانات كل المتغيرات مرةً واحدةً فقط. مرّرنا العدد ‎1‎ إلى المعامل ‎x‎ والعدد ‎2‎ إلى المعامل ‎y‎ والعدد ‎3‎ إلى المعامل ‎z‎، إذ تتوافق هذه القيم مع المعامِلات المقابلة لها في ترتيب الظهور، ويُجري البرنامج العمليات الحسابية على المعامِلات على النحو التالي: a = 1 + 2 b = 1 + 3 c = 2 + 3 تطبع الدالة أيضًا ‎a‎ و ‎b‎ و ‎c‎، وبناءً على العمليات الحسابية أعلاه، فستساوي قيمة ‎a‎ العدد ‎3‎ و ‎b‎ العدد ‎4‎ و ‎c‎ العدد ‎5‎، ولننفّذ البرنامج سنكتب ما يلي: $ go run add_numbers.go سيكون الخرج كما يلي: 3 4 5 عندما نمرر 1 و 2 و 3 على أساس معامِلات إلى الدالة ()addNumbers، فإننا نتلقى الناتج المتوقع. تُعَدّ المعامِلات وسائط تُعرَّف عادة على أساس متغيرات ضمن تعريف الدالة، كما يمكن تعيين قيم إليها عند تنفيذ التابع بتمرير وسائط إلى الدالة. إعادة قيمة يمكن تمرير قيم إلى الدالة ويمكن كذلك أن تُنتج الدالة قيمةً وتُعيدها لمن استدعاها، إذ يمكن أن تنتج الدالة قيمةً عبر استخدام التعليمة ‎return‎ وهي تعليمة اختيارية، ولكن في حال استخدامها ستُنهي الدالة عملها مباشرةً وتوقف تنفيذها وتُمرَّر قيمة التعبير الذي يعقُبها اختياريًا إلى المستدعي، كما يجب تحديد نوع البيانات المُعادة أيضًا. استخدمنا حتى الآن الدالة ()fmt.Println‎ بدلاً من التعليمة ‎return‎ في دوالنا لطباعة شيء بدلًا من إعادته، فلننشئ برنامجًا يعيد متغيرًا بدلًا من طباعته مباشرةً، لذا سننشئ برنامجًا في ملف نصي جديد يسمى double.go‎ يحسب ناتج مُضاعفة المعامِل ‎x‎ ويُسند الناتج إلى المتغير ‎y‎ ثم يعيده، كما سنطبع المتغير ‎result‎ والذي يساوي ناتج تنفيذ الدالة double(3)‎. package main import "fmt" func main() { result := double(3) fmt.Println(result) } func double(x int) int { y := x * 2 return y } لننفّذ البرنامج كما يلي: $ go run double.go سيكون الخرج كما يلي: 6 خرج البرنامج هو العدد الصحيح ‎6 الذي أعادته الدالة وهو ما نتوقعه إذا طلبنا من جو حساب ناتج ضرب 2 بالعدد 3. إذا حددنا نوع القيمة المُعادة فيجب علينا إعادة قيمة من هذا النوع وإلا فسيُعطي البرنامج خطأً في التصريف، ففي المثال التالي سنلغي تعليمة الإعادة المُستخدَمة في الشيفرة السابقة بوضع تعليق على تعليمة الإعادة لكي يتجاهلها المُصرّف كما يلي: package main import "fmt" func main() { result := double(3) fmt.Println(result) } func double(x int) int { y := x * 2 // return y } لنحاول تنفيذ البرنامج: $ go run double.go سيُشير الخرج إلى خطأ لأنه لم يجد تعليمة الإعادة return: ./double.go:13:1: missing return at end of function لا يمكن تصريف البرنامج بدون تعليمة الإعادة هذه، فعندما تصل الدالة إلى تعليمة return فإنها ستُنهي تنفيذ الدالة حتى إذا كان هناك تعليمات تالية ضمنها: package main import "fmt" func main() { loopFive() } func loopFive() { for i := 0; i < 25; i++ { fmt.Print(i) if i == 5 { //i == 5 أوقف الدالة عندما return } } fmt.Println("This line will not execute.") // هذا السطر لن ينفَّذ } نستخدِم هنا حلقة for تُؤدي عملية تكرار 25 مرة وبداخلها تعليمة if تتحقق مما إذا كانت قيمة i تساوي العدد 5، فإذا كانت كذلك، فسيكون لدينا تعليمة return تُنهي تنفيذ الحلقة وتُنهي تنفيذ الدالة أيضًا، وهذا يعني أنّ باقي التكرارات لن تُنفّذ والسطر الأخير من الدالة This line will not execute لن يُنفّذ. يؤدي استخدام التعليمة ‎return‎ داخل الحلقة ‎for‎ إلى إنهاء الدالة، وبالتالي لن يُنفَّذ السطر الموجود خارج الحلقة، فإذا استخدمنا بدلًا من ذلك التعليمة break، فسيُنفّذ السطر fmt.Println()‎ الأخير من المثال السابق. نعيد التذكير أنَّ التعليمة ‎return‎ تنهي عمل الدالة وقد تعيد قيمةً إذا أعقبها تعبير وكان ذلك محدد في تعريف الدالة. إعادة عدة قيم يمكن للدوال أن تُعيد أكثر من قيمة، إذ سنجعل برنامج repeat.go يُعيد قيمتين بحيث تكون القيمة الأولى القيمة المُكررة والثانية خطأً في حال كانت قيمة المعامِل reps أصغر أو تساوي 0. package main import "fmt" func main() { val, err := repeat("Sammy", -1) if err != nil { fmt.Println(err) return } fmt.Println(val) } func repeat(word string, reps int) (string, error) { if reps <= 0 { return "", fmt.Errorf("invalid value of %d provided for reps. value must be greater than 0.", reps) } var value string for i := 0; i < reps; i++ { value = value + word } return value, nil } تتحقق الدالة repeat بدايةً من أن الوسيط reps صالح، فأيّ قيمة أقل أو تساوي الصفر ستؤدي إلى خطأ. بما أننا مررنا القيمة 1- إلى reps فهذا سيؤدي إلى تحقق شرط حدوث خطأ، وبالتالي ستُعاد قيمتين الأولى هي سلسلة فارغة "" والثانية هي رسالة خطأ، وبالطبع يجب علينا إعادة قيمتين دومًا تبعًا للتعريف الذي وضعناه في ترويسة الدالة، إذ حددنا أنّ هناك قيمتان مُعادتان من الدالة؛ فالأولى هي سلسلة والثانية هي خطأ، ولهذا السبب أعدنا سلسلةً فارغةً في حال ظهور خطأ. نستدعي الدالة repeat بداخل الدالة main ونُسند القيم المُعادة إلى متغيرين هما value و err، وبما أنّ هناك احتمال لحدوث خطأ، فسنتحقق في الأسطر التالية من وجوده، وفي حال وجوده سنطبع رسالةً تشير إلى الخطأ وسنستخدِم التعليمة return للخروج من الدالة main والبرنامج، وبتنفيذ البرنامج سنحصل على ما يلي: invalid value of -1 provided for reps. value must be greater than 0. ملاحظة: من السلوكيات الجيدة في البرمجة إعادة قيمتان أو ثلاثة، بالإضافة إلى إعادة كل الأخطاء كآخر قيمة معادة من الدالة. تعلّمنا في هذا القسم كيف نجعل تعليمة return تُعيد أكثر من قيمة. الدوال المرنة Variadic الدالة المرنة Variadic هي دالة لا تقبل أي قيمة أو تقبل قيمةً واحدةً أو قيمتين أو أكثر على أساس وسيط واحد، والدوال المرنة ليست شائعة الاستخدام لكنها تجعل الشيفرة أنظف وأكثر قابليةً للقراءة، وأحد الأمثلة على هذا النوع من الدوال هو الدالة Println من الحزمة fmt: func Println(a ...interface{}) (n int, err error) نسمي الدالة التي تتضمن مُعامِلًا مُلحقًا بثلاثة نقاط ... كما في الشيفرة أعلاه بالدالة المرنة، إذ تشير النقاط الثلاث إلى أنّ هذا المعامِل يمكن أن يكون صفر قيمة أو قيمةً واحدةً أو قيمتين أو عدة قيم، وبالتالي تُعَدّ الدالة fmt.Println دالةً مرنةً لأنها تتضمن مُعامِلًا مرنًا يسمى a. سنستخدِم في المثال التالي الدالة المرنة السابقة وسنبيّن كيف أنه من الممكن أن نمرر لها عددًا غير مُحدد من الوسائط: package main import "fmt" func main() { fmt.Println() fmt.Println("one") fmt.Println("one", "two") fmt.Println("one", "two", "three") } كما تُلاحظ فإننا نستدعيها أول مرة بدون تمرير أيّ وسيط ثم نستدعيها مع تمرير وسيط واحد هو السلسلة one ثم نستدعيها مع وسيطين ثم نستدعيها مع ثلاثة وسائط، ولننفذ البرنامج الآن: $ go run print.go سيكون الخرج كما يلي: one one two one two three لاحظ أنّ السطر الأول من الخرج فارغ لأننا استدعينا تعليمة الطباعة بدون تمرير أيّ متغير ثم السطر الثاني يحتوي على one لأن دالة الطباعة التي استدعيناها في المرة الثانية تتضمنها، ثم one two لأننا مررناهما إلى دالة الطباعة في المرة الثالثة، وأخيرًا السطر الأخير one two three لأننا مررنا هذه الكلمات إلى دالة الطباعة في الشيفرة السابقة. سنتحدّث الآن عن كيفية تعريف هذه الدوال بعد أن اطلعنا على كيفية استخدام الدوال المرنة. تعريف الدوال المرنة كما ذكرنا سابقًا فإن الدوال المرنة تُعرّف من خلال وضع ثلاث نقاط ... بعد اسم أحد المعامِلات فيها، وفي المثال التالي سنعرّف دالةً نمرر لها أسماءً لكي تُحَييهم: package main import "fmt" func main() { sayHello() sayHello("Sammy") sayHello("Sammy", "Jessica", "Drew", "Jamie") } func sayHello(names ...string) { for _, n := range names { fmt.Printf("Hello %s\n", n) } } تتضمّن الدالة sayHello معامِلًا اسمه names من النوع string، وكما نلاحظ فإنه توجد ثلاث نقاط بعد اسمه، وبالتالي هو معامل مرن أي يقبل عدد غير مُحدد من الوسائط، وبالتالي تُعَدّ الدالة sayHello هي دالةً مرنةً. تُعامِل هذه الدالة المعامِل names على أنه شريحة من الأسماء، أي أنها تعامله على أساس شريحة من السلاسل النصية string[]، وبالتالي يمكننا التكرار عليه بحلقة for من خلال استخدام العامِل range. ستحصل عند تنفيذ هذه الشيفرة على ما يلي: Hello Sammy Hello Sammy Hello Jessica Hello Drew Hello Jamie لاحظ أنه في المرة الأولى التي استدعينا فيها الدالة sayHello لم يُطبع أيّ شيء، وذلك لأننا لم نمرر لها أيّ قيمة، أي عمليًّا هي شريحة فارغة، وبالتالي لا تتضمن أي قيمة ليُكرر عليها، والآن سنُعدّل الشيفرة السابقة بحيث تطبع عبارةً مُحددةً عندما لا تُمرّر أيّ قيمة: package main import "fmt" func main() { sayHello() sayHello("Sammy") sayHello("Sammy", "Jessica", "Drew", "Jamie") } func sayHello(names ...string) { if len(names) == 0 { fmt.Println("nobody to greet") return } for _, n := range names { fmt.Printf("Hello %s\n", n) } } أضفنا تعليمة if تتحقق مما إذا كانت الشريحة فارغةً وهذا يُكافئ أن يكون طولها 0، وفي هذه الحالة سنطبع nobody to greet: nobody to greet Hello Sammy Hello Sammy Hello Jessica Hello Drew Hello Jamie يجعل استخدام الدوال والمتغيرات المرنة شيفرتك أكثر قابليةً للقراءة، وسنُنشئ الآن دالةً مرنةً تربط الكلمات اعتمادًا على رمز مُحدّد، لكن سنكتب أولًا دالةً ليست مرنة لنُبيّن الفرق: package main import "fmt" func main() { var line string line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"}) fmt.Println(line) line = join(",", []string{"Sammy", "Jessica"}) fmt.Println(line) line = join(",", []string{"Sammy"}) fmt.Println(line) } func join(del string, values []string) string { var line string for i, v := range values { line = line + v if i != len(values)-1 { line = line + del } } return line } مرّرنا هنا الفاصلة , إلى الدالة join لكي يُنجز الربط اعتمادًا عليها، ثم مرّرنا شريحةً من الكلمات لكي تُربط، فكان الخرج كما يلي: Sammy,Jessica,Drew,Jamie Sammy,Jessica Sammy لاحظ هنا أنّ تعريف شريحة في كل مرة نستدعي فيها هذه الدالة قد يكون مُملًا وأكثر صعوبةً في القراءة، لذا سنُعرّف الآن الشيفرة نفسها لكن مع دالة مرنة: package main import "fmt" func main() { var line string line = join(",", "Sammy", "Jessica", "Drew", "Jamie") fmt.Println(line) line = join(",", "Sammy", "Jessica") fmt.Println(line) line = join(",", "Sammy") fmt.Println(line) } func join(del string, values ...string) string { var line string for i, v := range values { line = line + v if i != len(values)-1 { line = line + del } } return line } سنحصل عند تشغيل البرنامج على خرج المثال السابق نفسه: Sammy,Jessica,Drew,Jamie Sammy,Jessica Sammy يمكن بسهولة ملاحظة أنّ استخدام مفهوم الدالة المرنة قد جعل من الدالة join أكثر قابليةً للقراءة. ترتيب الوسائط المرنة يمكنك تعريف معامِل مرن واحدة فقط في الدالة ويجب أن يكون هو المعامِل الأخير في ترويسة الدالة، فإذا عرّفتَ أكثر من معامِل مرن أو وضعته قبل المعامِلات العادية، فسيظهر لك خطأ وقت التصريف compilation error. package main import "fmt" func main() { var line string line = join(",", "Sammy", "Jessica", "Drew", "Jamie") fmt.Println(line) line = join(",", "Sammy", "Jessica") fmt.Println(line) line = join(",", "Sammy") fmt.Println(line) } func join(values ...string, del string) string { var line string for i, v := range values { line = line + v if i != len(values)-1 { line = line + del } } return line } وضعنا هنا المعامِل المرن values أولًا ثم وضعنا المعامِل العادي del، وبالتالي خالفنا الشرط المذكور سلفًا، وبالتالي سنحصل على الخطأ التالي: ./join_error.go:18:11: syntax error: cannot use ... with non-final parameter values عند تعريف دالة مرنة لا يمكن أن يكون المعامِل الأخير إلا معاملًا مرنًا. تفكيك الوسائط رأينا كيف أنّ المعامل المرن سيسمح لنا بتمرير 0 قيمة أو قيمة واحدة أو أكثر من قيمة إلى الدالة، لكن هناك حالات سيكون لدينا فيها شريحة من القيم نريد تمريرها إلى الدالة المرنة، لذا دعونا نرى الدالة join التي بنيناها مؤخرًا لرؤية ما يحدث: package main import "fmt" func main() { var line string names := []string{"Sammy", "Jessica", "Drew", "Jamie"} line = join(",", names) fmt.Println(line) } func join(del string, values ...string) string { var line string for i, v := range values { line = line + v if i != len(values)-1 { line = line + del } } return line } سنحصل عند تشغيل البرنامج على خطأ وقت التصريف: ./join-error.go:10:14: cannot use names (type []string) as type string in argument to join على الرغم من أن الدالة المرنة ستحوّل المعامِل values ...string إلى شريحة من السلاسل النصية string[]، إلا أنّ هذا لا يعني أنه بإمكاننا تمرير شريحة من السلاسل على أساس وسيط، فالمُصرّف يتوقع وسائط منفصلةً من نوع سلسلة نصية. يمكننا لحل هذه المشكلة تفكيك قيم الشريحة -أي فصلها عمليًّا- من خلال وضع ثلاثة نقط بعد اسم الشريحة عندما نُمررها لدالة مرنة. package main import "fmt" func main() { var line string names := []string{"Sammy", "Jessica", "Drew", "Jamie"} line = join(",", names...) fmt.Println(line) } func join(del string, values ...string) string { var line string for i, v := range values { line = line + v if i != len(values)-1 { line = line + del } } return line } لاحظ أننا وضعنا 3 نقاط بعد اسم الشريحة names عندما مررناها إلى الدالة join، وهذا يؤدي إلى تفكيك عناصر الشريحة، وبالتالي كأنها قيم منفصلة، وسيكون الخرج كما يلي: Sammy,Jessica,Drew,Jamie لاحظ أنه مازال بإمكاننا عدم تمرير أيّ شيء أو تمرير أيّ عدد نريده من القيم، ويبيّن المثال التالي كل الحالات: package main import "fmt" func main() { var line string line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"}...) fmt.Println(line) line = join(",", "Sammy", "Jessica", "Drew", "Jamie") fmt.Println(line) line = join(",", "Sammy", "Jessica") fmt.Println(line) line = join(",", "Sammy") fmt.Println(line) } func join(del string, values ...string) string { var line string for i, v := range values { line = line + v if i != len(values)-1 { line = line + del } } return line } سيكون الخرج كما يلي: Sammy,Jessica,Drew,Jamie Sammy,Jessica,Drew,Jamie Sammy,Jessica Sammy أصبحنا الآن نعرف كيف نمرِّر 0 قيمة أو أكثر إلى دالة مرنة، كما أصبحنا نعرف كيف يمكننا تمرير شريحة إلى دالة مرنة. غالبًا ما تكون الدوال المرنة مفيدةً في الحالات التالية: عندما تكون بحاجة إلى تعريف شريحة مؤقتًا من أجل تمريرها إلى دالة. لجعل الشيفرة أكثر قابليةً للقراءة. عندما يكون عدد الوسائط غير معروف أو متغير مثل دالة الطباعة. الخاتمة تُعَدّ الدوال كتلًا من التعليمات البرمجية التي تُنفِّذ إجراءات معيّنة داخل البرنامج، كما تساعد على جعل الشفرة تركيبيةً وقابلةً لإعادة الاستخدام، بالإضافة إلى أنها تنظمها وتسهل من قراءتها، كما تجعل الدوال المرنة الشيفرة أكثر قابليةً للقراءة، إلا أنها ليست شائعة الاستخدام، ولتتعلم كيف تجعل برنامجك تركيبيًّا أكثر، فيمكنك قراءة مقال كيفية تعريف الحزم في لغة جو Go. ترجمة -وبتصرف- للمقالَين How To Define and Call Functions in Go وللمقال How To Use Variadic Functions in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: التعامل مع حلقة التكرار For في لغة جو Go استخدام المتغيرات والثوابت في لغة جو Go
  18. يسمح لنا استخدام حلقات التكرار في برمجة الحاسوب بأتمتة وتكرار المهام المتشابهة مرات عدة ريثما يتحقق شرط معيّن أو ظهور حدث محدد، فتخيل إذا كان لديك قائمة بالملفات التي تحتاج إلى معالجتها أو إذا كنت تريد حساب عدد الأسطر في المقالة أو إدخال 10 أعداد أو أسماء من المُستخدِم، فيمكنك استخدام حلقة في التعليمات البرمجية الخاصة بك لحل هذه الأنواع من المشاكل. سنشرح في هذا المقال كيفية استخدام حلقة for في لغة جو، وهي الحلقة الوحيدة المتوفرة فيها على عكس اللغات الأخرى التي تتضمن عدة أنواع من الحلقات مثل حلقة while أو do، فالهدف من وجود نوع حلقة واحدة هو التبسيط وتجنب الارتباك بين المبرمجين من الأنواع المختلفة، إذ نكتفي بواحدة فقط بدلًا من وجود عدة أنواع للحلقات تحقق الغرض نفسه وبالتكلفة نفسها. تؤدي حلقة for إلى تكرار تنفيذ جزء من الشيفرات بناءً على عدّاد أو على متغير، وهذا يعني أنّ حلقات for تستعمل عندما يكون عدد مرات تنفيذ حلقة التكرار معلومًا قبل الدخول في الحلقة، لكن في حقيقة الأمر يمكنك استخدامها حتى إذا لم يكن عدد التكرارات معلومًا من خلال استخدام تعليمة break كما سترى لاحقًا. سنبدأ بالحديث في هذا المقال عن الأنواع المختلفة من هذه الحلقة ثم كيفية التكرار على أنواع البيانات التسلسلية مثل البيانات النصية Strings ثم الحلقات المتداخلة. التصريح عن حلقة For حددت لغة جو 3 طرق مختلفة لإنشاء الحلقات كُلٌّ منها بخصائص مختلفة بوضع كل حالات الاستخدام الممكنة للحلقات في الحسبان، وهي إنشاء حلقة for مع شرط Condition أو ForClause أو RangeClause، إذ سنشرح في هذا القسم كيفية تصريح واستخدام أنواع ForClause و Condition، لذا دعونا نلقي نظرةً على كيفية استخدام حلقة for مع ForClause أولًا. تُعرّف حلقة ForClause من خلال كتابة تعليمة التهيئة initial statement متبوعة بشرط Condition متبوعة بتعليمة التقدّم Post Statement: for [ Initial Statement ] ; [ Condition ] ; [ Post Statement ] { [Action] } لكي نفهم المكونات السابق سنرى المثال التالي الذي يعرض حلقة for تتزايد ضمن نطاق محدد من القيم باستخدام التعريف السابق: for i := 0; i < 5; i++ { fmt.Println(i) } يتمثل الجزء الأول من الحلقة في تعليمة التهيئة i := 0 والتي تعبر عن تصريح متغير اسمه i يُسمى متغير الحلقة وقيمته الأولية هي 0؛ أما الجزء الثاني فهو الشرط i < 5 والذي يُمثّل الشرط الذي يجب أن يتحقق لكي ندخل في الحلقة أو في التكرار التالي من الحلقة، إذ يمكن القول ادخُل في الحلقة ونفّذ محتواها طالما أنّ قيمة i أصغر من 5، والجزء الأخير هو تعليمة التقدّم ++i والتي تشير إلى التعديل الذي سنجريه على متغير الحلقة i بعد انتهاء كل تكرار، إذ يكون التعديل هنا هو زيادته بمقدار واحد. سيكون خرج البرنامج كما يلي: 0 1 2 3 4 تتكرر الحلقة 5 مرات، بدايةً تكون قيمة i تساوي 0 وهي أصغر من 5، لذا ندخل في الحلقة وننفّذ محتواها (fmt.Println(i ثم نزيد قيمة متغير الحلقة بواحد فتصبح قيمته 1 وهي أصغر من 5، ثم ننفّذ محتوى الحلقة مرةً أخرى ونزيدها بواحد فتصبح 2 وهي أصغر من 5 وهكذا إلى أن تصبح قيمة متغير الحلقة 5 فينتهي تنفيذ الحلقة. ملاحظة: تبدأ الفهرسة بالقيمة 0، لذا ستكون قيمة i في المرة الخامسة للتنفيذ هي 4 وليس 5. طبعًا ليس بالضرورة أن تبدأ بقيمة محددة مثل الصفر أو تنتهي بقيمة محددة وإنما يمكنك تحديد أي نطاق تريده كما في هذا المثال: for i := 20; i < 25; i++ { fmt.Println(i) } سنبدأ هنا بالقيمة الأولية 20 وننتهي عند الوصول إلى القيمة 25 بحيث تكون أقل تمامًا من 25، أي ننتهي بالعدد 24: 20 21 22 23 24 يمكنك أيضًا التحكم بخطوات الحلقة على عداد الحلقة كما تريد، إذ يمكنك مثلًا تقديم قيمة المتغير بثلاثة بعد كل تكرار كما في المثال التالي: for i := 0; i < 15; i += 3 { fmt.Println(i) } نبدأ هنا بالعدد 0 وننتهي عند الوصول إلى 15، وبعد كل تكرار نزيد العدّاد (متغير الحلقة) بثلاثة، وسيكون الخرج كما يلي: 0 3 6 9 12 يمكننا أيضًا التكرار بصورة عكسية، أي البدء بقيمة كبيرة والعودة خلفًا، إذ سنبدأ في المثال التالي بالعدد 100 وننتهي عند الوصول إلى 0، وهنا لابد من أن يكون التقدم عكسيًا، أي باستخدام عملية الطرح وليس الجمع: for i := 100; i > 0; i -= 10 { fmt.Println(i) } وضعنا هنا القيمة الأولية على 100 ووضعنا شرطًا للتوقف i < 0 عند الصفر وجعلنا التقدم عكسيًا بمقدار 10 إلى الخلف بحيث سينقص 10 من قيمة متغير الحلقة بعد كل تكرار، وبالتالي سيكون الخرج كما يلي: 100 90 80 70 60 50 40 30 20 10 يمكنك أيضًا الاستغناء عن كتابة القيمة الأولية للمتغير وعن مقدار التقدّم وترك الشرط فقط، إذ نسمي هذا النوع من الحلقات بحلقة الشرط Condition loop والتي تكافئ الحلقة while في لغات البرمجة الأخرى: i := 0 for i < 5 { fmt.Println(i) i++ } صرّحنا هنا عن المتغير i قبل الدخول إلى الحلقة، إذ يُنفّذ ما بداخل الحلقة طالما أنّ الشرط محقق، أي i أصغر من 5، ولاحظ أيضًا أننا نزيد من قيمة المتغير i في نهاية الكتلة البرمجية الخاصة بالحلقة، فذا لم نزده، فسندخل في حلقة لانهائية، وفي الواقع نحن لم نستغنِ عن تعليمة التهيئة أو التقدّم (الخطوات)، وإنما عرّفناهم بطريقة مختلفة، والغاية من هذا التعريف هو المطابقة مع حلقة while المستخدَمة في باقي اللغات. ذكرنا التعليمة break في بداية المقال والتي سنستخدِمها عندما يكون شرط التوقف غير مرتبط بعدد محدد من التكرارات وإنما بحدث ما مثل تكرار عملية إدخال كلمة المرور من المستخدِم طالما ليست صحيحةً، ويمكن التعبير عن هكذا حلقة من خلال كتابة for فقط كما يلي: for { if someCondition { break } // do action here } مثال آخر على ذلك عندما نقرأ من بنية غير محددة الحجم مثل المخزِّن المؤقت buffer ولا نعرف متى سننتهي من القراءة: package main import ( "bytes" "fmt" "io" ) func main() { buf := bytes.NewBufferString("one\ntwo\nthree\nfour\n") for { line, err := buf.ReadString('\n') if err != nil { if err == io.EOF { fmt.Print(line) break } fmt.Println(err) break } fmt.Print(line) } } يُصرّح في السطر ("buf :=bytes.NewBufferString("one\ntwo\nthree\nfour\n عن مخزِّن مؤقت مع بعض البيانات، ونظرًا لأننا لا نعرف متى سينتهي المخزِّن المؤقت من القراءة، سننشئ حلقة for بدون أيّ بند، أي بدون شرط أو قيمة أولية أو مقدار تقدّم. نستخدِم داخل الحلقة ('line, err := buf.ReadString('\n لقراءة سطر من المخزِّن المؤقت والتحقق مما إذا كان هناك خطأ في القراءة منه، فإذا كان هناك خطأ، فسنعالج الخطأ ونستخدِم الكلمة المفتاحية break للخروج من حلقة for، فكما نلاحظ أننا من خلال تعليمة break لم نعُد بحاجة إلى كتابة بند الشرط لإيقافها. تعلمنا في هذا المثال كيفية التصريح عن حلقة ForClause وكيفية استخدامها للتكرار على مجال محدد من القيم، كما تعلمنا أيضًا كيفية استخدام حلقة الشرط للتكرار حتى يتحقق شرط معيّن. التكرار على أنواع البيانات المتسلسلة باستخدام RangeClause من الشائع في جو استخدام الحلقات للتكرار على عناصر أنواع البيانات المتسلسلة أو التجميعية مثل الشرائح والمصفوفات والسلاسل النصية، ولتسهيل هذه العملية يمكننا استخدام حلقة for مع بنية RangeClause، ويمكنك عمومًا استخدام بنية ForClause للتكرار على أيّ نوع من البيانات بطرق محددة طبعًا، إلا أن بنية RangeClause تُعَدّ مُنظّمَةً أكثر وأسهل في القراءة. بدايةً سنلقي نظرةً على كيفية التكرار باستخدام ForClause في هكذا حالات: package main import "fmt" func main() { sharks := []string{"hammerhead", "great white", "dogfish", "frilled", "bullhead", "requiem"} for i := 0; i < len(sharks); i++ { fmt.Println(sharks[i]) } } بتشغيل الشيفرة السابقة سنحصل على الخرج التالي: hammerhead great white dogfish frilled bullhead requiem سنستخدِم الآن بنية RangeClause لتنفيذ الإجراءات نفسها: package main import "fmt" func main() { sharks := []string{"hammerhead", "great white", "dogfish", "frilled", "bullhead", "requiem"} for i, shark := range sharks { fmt.Println(i, shark) } } سيُطبع في هذه الحالة كل عنصر في القائمة. هنا استخدمنا المتغيرين i و shark من أجل عملية التكرار ويمكنك استخدام أيّ اسم آخر لهذه المتغيرات، إذ تعيد الكلمة المفتاحية range قيمتين؛ الأولى هي فهرس العنصر والثانية هي قيمته، وبالتالي سيكون الخرج كما يلي: 0 hammerhead 1 great white 2 dogfish 3 frilled 4 bullhead 5 requiem قد لا نكون بحاجة إلى عرض فهرس العنصر في بعض الأحيان، لكن إذا حذفناها، فسنتلقى خطأً في وقت التصريف: package main import "fmt" func main() { sharks := []string{"hammerhead", "great white", "dogfish", "frilled", "bullhead", "requiem"} for i, shark := range sharks { fmt.Println(shark) } } سيكون الخرج كما يلي: src/range-error.go:8:6: i declared and not used نظرًا لأنّ i قد صُرِّح عنه في الحلقة for، ولكن لم يُستخدم مطلقًا، فسوف يُعطي المُصرّف خطأً يُشير إلى أن هناك متغيرًا قد صُرّح عنه ولم يُستخدَم، وهذا الخطأ ستتلقاه في جو في أيّ وقت تُصرّح فيه عن متغير ولا تستخدِمه، ولحل هذه المشكلة سنستخدِم المتغير المجهول الذي يُعبَّر عنه بالشرطة السفلية _ والذي يشير إلى أنّ هناك قيمة ستُعاد ونعرف ذلك لكن لا نريدها، لذا سنستبدل المتغير i بالمتغير المجهول وسينجح الأمر: package main import "fmt" func main() { sharks := []string{"hammerhead", "great white", "dogfish", "frilled", "bullhead", "requiem"} for _, shark := range sharks { fmt.Println(shark) } } سيكون الخرج كما يلي: hammerhead great white dogfish frilled bullhead requiem يوضِّح الخرج أنّ حلقة for قد تكررت على شريحة السلاسل وطُبع كل عنصر من الشريحة بدون الفهرس. يمكنك أيضًا استخدام الكلمة range لإضافة عناصر إلى قائمة ما: package main import "fmt" func main() { sharks := []string{"hammerhead", "great white", "dogfish", "frilled", "bullhead", "requiem"} for range sharks { sharks = append(sharks, "shark") } fmt.Printf("%q\n", sharks) } سيكون الخرج كما يلي: ['hammerhead', 'great white', 'dogfish', 'frilled', 'bullhead', 'requiem', 'shark', 'shark', 'shark', 'shark', 'shark', 'shark'] أضفنا هنا السلسة 'shark' إلى الشريحة sharks عدة مرات من خلال التكرار عليها، أي أضفنا إليها x مرة، بحيث تكون x هي طول القائمة التي نُكرر عليها، ولاحظ هنا أننا لسنا بحاجة إلى استخدام المتغير المجهول _ إطلاقًا، وذلك لأن هكذا حالة مُعرّفة ضمن جو، كما أننا لم نستخدِم أيّ متغير. يمكننا أيضًا استخدام العامِل range لملء قيم الشريحة كما يلي: package main import "fmt" func main() { integers := make([]int, 10) fmt.Println(integers) for i := range integers { integers[i] = i } fmt.Println(integers) } نُصرِّح هنا عن الشريحة integers على أنها شريحة من الأعداد الصحيحة int ونحجز مساحةً لعشرة عناصر مُسبقًا ثم نطبع الشريحة ثم نملأ هذه الشريحة بقيم عناصر فهارسها، ثم نطبعها من جديد: [0 0 0 0 0 0 0 0 0 0] [0 1 2 3 4 5 6 7 8 9] نلاحظ أنه في المرة الأولى للطباعة تكون عناصر الشريحة صفرية، ﻷننا حجزنا 10 عناصر ولم نُعطهم قيمًا، أي القيمة الافتراضية هي 0، لكن في المرة التالية تكون قيم الشريحة من 0 إلى 9 وهذا يُكافئ قيم الفهارس لها. يمكننا أيضًا استخدام العامِل range للتكرار على محارف سلسلة: package main import "fmt" func main() { sammy := "Sammy" for _, letter := range sammy { fmt.Printf("%c\n", letter) } } سيكون الخرج كما يلي: S a m m y عند استخدام range للتكرار على عناصر رابطة map، فستُعاد قيمة كل من المفتاح والقيمة: package main import "fmt" func main() { sammyShark := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"} for key, value := range sammyShark { fmt.Println(key + ": " + value) } } سيكون الخرج كما يلي: color: blue location: ocean name: Sammy animal: shark ملاحظة: عناصر الرابطة غير مُرتبة، وبالتالي ستُطبع عناصرها بترتيب عشوائي في كل مرة تُنفّذ فيها الشيفرة. الآن بعد أن تعرّفنا على كيفية التكرار على العناصر المتسلسة، سنتعلم كيفية استخدام الحلقات المتداخلة. الحلقات المتداخلة Nested Loops يمكن لحلقات التكرار في جو أن تداخل كما هو الحال في بقية لغات البرمجة، فحلقة التكرار المتداخلة هي الحلقة الموجودة ضمن حلقة تكرار أخرى وهذا مفيد لتكرار عدة عمليات على كل عنصر مثلًا وهي شبيهة بتعليمات if المتداخلة، وتُبنى حلقات التكرار المتداخلة كما يلي: for { [Action] for { [Action] } } يبدأ البرنامج بتنفيذ حلقة التكرار الخارجية ويُنفَّذ أول تكرار فيها والذي سيؤدي إلى الدخول إلى حلقة التكرار الداخلية، مما يؤدي إلى تنفيذها إلى أن تنتهي تمامًا، بعد ذلك سيعود تنفيذ البرنامج إلى بداية حلقة التكرار الخارجية، ويبدأ بتنفيذ التكرار الثاني ثم سيصل التنفيذ إلى حلقة التكرار الداخلية، وستُنفَّذ حلقة التكرار الداخلية بالكامل، ثم سيعود التنفيذ إلى بداية حلقة التكرار الخارجية، وهكذا إلى أن ينتهي تنفيذ حلقة التكرار الخارجية أو إيقاف حلقة التكرار عبر استخدام break أو غيرها من التعليمات. لنُنشِئ مثالًا يستعمل حلقة for متداخلة لكي نفهم كيف تعمل بدقة، حيث ستمر حلقة التكرار الخارجية في المثال الآتي على شريحة من الأعداد اسمها numList؛ أما حلقة التكرار الداخلية فستمر على شريحة من السلاسل النصية اسمها alphaList: package main import "fmt" func main() { numList := []int{1, 2, 3} alphaList := []string{"a", "b", "c"} for _, i := range numList { fmt.Println(i) for _, letter := range alphaList { fmt.Println(letter) } } } سيظهر الناتج التالي عند تشغيل البرنامج: 1 a b c 2 a b c 3 a b c يُظهِر الناتج السابق أنَّ البرنامج قد أكمل أول تكرار على عناصر حلقة التكرار الخارجية بطباعة العدد، ومن ثم بدأ بتنفيذ حلقة التكرار الداخلية، لذا طبع المحارف a و b و c على التوالي، وبعد انتهاء تنفيذ حلقة التكرار الداخلية، عاد البرنامج إلى بداية حلقة التكرار الخارجية طابعًا العدد 2، ثم بدأ تنفيذ حلقة التكرار الداخلية، مما يؤدي إلى إظهار a و b و c مجددًا وهكذا. يمكن الاستفادة من حلقات for المتداخلة عند المرور على عناصر شريحة تتألف من شرائح، فإذا استعملنا حلقة تكرار وحيدة لعرض عناصر شريحة تتألف من عناصر تحتوي على شرائح، فستُعرَض قيم الشرائح الداخلية على أساس عنصر: package main import "fmt" func main() { ints := [][]int{ []int{0, 1, 2}, []int{-1, -2, -3}, []int{9, 8, 7}, } for _, i := range ints { fmt.Println(i) } } سيكون الخرج كما يلي: [0 1 2] [-1 -2 -3] [9 8 7] إذا أردنا الوصول إلى العناصر الموجودة في الشرائح الداخلية، فيمكننا استعمال حلقة for متداخلة: package main import "fmt" func main() { ints := [][]int{ []int{0, 1, 2}, []int{-1, -2, -3}, []int{9, 8, 7}, } for _, i := range ints { for _, j := range i { fmt.Println(j) } } } سيكون الخرج كما يلي: 0 1 2 -1 -2 -3 9 8 7 نستطيع الاستفادة من حلقات for المتداخلة عندما نريد التكرار على عناصر قابلة للتكرار داخل الشرائح. استخدام تعليمات continue و break تسمح لك حلقة for بأتمتة وتكرار المهام بطريقة فعّالة، لكن في بعض الأحيان قد يتدخل عامل خارجي في طريقة تشغيل برنامجك، وعندما يحدث ذلك، فربما تريد من برنامجك الخروج تمامًا من حلقة التكرار أو تجاوز جزء من الحلقة قبل إكمال تنفيذها أو تجاهل هذا العامِل الخارجي تمامًا، لذا يمكنك فعل ما سبق باستخدام تعابير break و continue. تعليمة break توفِّر لك تعليمة break -والتي تعاملنا معها سابقًا- القدرة على الخروج من حلقة التكرار عند حدوث عامل خارجي، وتوضَع عادةً ضمن تعبير if، وفيما يلي أحد الأمثلة الذي يستعمل تعليمة break داخل حلقة for: package main import "fmt" func main() { for i := 0; i < 10; i++ { if i == 5 { fmt.Println("Breaking out of loop") break // break here } fmt.Println("The value of i is", i) } fmt.Println("Exiting program") } بنينا حلقة تكرار for التي تعمل طالما كانت قيمة المتغير i أصغر من 10 وحددنا التقدّم بمقدار 1 بعد كل تكرار، ثم لدينا تعليمة if التي تختبر فيما إذا كان المتغير i مساوٍ للعدد 5، فعند حدوث ذلك، فستُنفَّذ break للخروج من الحلقة. توجد داخل حلقة التكرار الدالة fmt.Println()‎ التي تُنفَّذ في كل تكرار وتطبع قيمة المتغير i إلى أن نخرج من الحلقة عبر break وهنا يكون لدينا تعليمة طباعة أخرى تطبع Breaking out of loop، ولكي نتأكد أننا خرجنا من الحلقة تركنا تعليمة طباعة أخرى تطبع Exiting program، وبالتالي سيكون الخرج كما يلي: The value of i is 0 The value of i is 1 The value of i is 2 The value of i is 3 The value of i is 4 Breaking out of loop Exiting program يُظهر الناتج السابق أنَّه بمجرد أن أصبحت قيمة المتغير i مساويةً للعدد 5، فسينتهي تنفيذ حلقة التكرار عبر break. تعليمة continue تعليمة continue تسمح لنا بتخطي جزء من حلقة التكرار عند حدوث عامِل خارجي مع إكمال بقية الحلقة إلى نهايتها، أي سينتقل تنفيذ البرنامج إلى أوّل حلقة التكرار عند تنفيذ continue، وتوضَع عادةً ضمن تعليمة if. سنستخدِم البرنامج نفسه الذي استعملناه لشرح التعبير break أعلاه، لكننا سنستخدم التعبير continue بدلًا من break: package main import "fmt" func main() { for i := 0; i < 10; i++ { if i == 5 { fmt.Println("Continuing loop") continue // break here } fmt.Println("The value of i is", i) } fmt.Println("Exiting program") } الفرق الوحيد عن الشيفرة السابقة هي أننا استبدلنا break بتعليمة continue، وبالتالي لن يتوقف البرنامج عند وصول متغير الحلقة إلى 5، وإنما فقط سيتخطاها ويتابع التنفيذ، وبالتالي سيكون الخرج كما يلي: The value of i is 0 The value of i is 1 The value of i is 2 The value of i is 3 The value of i is 4 Continuing loop The value of i is 6 The value of i is 7 The value of i is 8 The value of i is 9 Exiting program نلاحظ أنَّ السطر الذي يجب أن يحتوي على The value of i is 5 ليس موجودًا في المخرجات، لكن سيُكمَل تنفيذ حلقة التكرار بعد هذه المرحلة مما يؤدي إلى طباعة الأعداد من 6 إلى 10 قبل إنهاء تنفيذ الحلقة. يمكنك استخدام التعليمة continue لتفادي استخدام تعابير شرطية معقدة ومتداخلة أو لتحسين أداء البرنامج عن طريق تجاهل الحالات التي ستُرفَض نتائجها، وبالتالي ستؤدي تعليمة continue إلى جعل البرنامج يتجاهل تنفيذ حلقة التكرار عند تحقيق شرط معيّن، لكن بعد ذلك سيُكمِل تنفيذ الحلقة مثل العادة. تعليمة break مع الحلقات المتداخلة من المهم أن تتذكر أنّ تعليمة break ستوقف فقط تنفيذ الحلقة الداخلية التي يتم استدعاؤها فيها، فإذا كان لديك مجموعة متداخلة من الحلقات، فستحتاج إلى تعليمة break لكل حلقة: package main import "fmt" func main() { for outer := 0; outer < 5; outer++ { if outer == 3 { fmt.Println("Breaking out of outer loop") break // break تعليمة } fmt.Println("The value of outer is", outer) for inner := 0; inner < 5; inner++ { if inner == 2 { fmt.Println("Breaking out of inner loop") break // break تعليمة } fmt.Println("The value of inner is", inner) } } fmt.Println("Exiting program") } تتكرر الحلقتان 5 مرات ولكل منهما تعليمة if مع تعليمة break، فالحلقة الخارجية سوف تنكسر أو تُنهى -أي يُطبّق عليها break- إذا كانت قيمة المتغير outer تساوي 3؛ أما الحلقة الداخلية فستنكسر إذا كانت قيمة المتغير inner تساوي 2، وبالتالي سيكون الخرج كما يلي: The value of outer is 0 The value of inner is 0 The value of inner is 1 Breaking out of inner loop The value of outer is 1 The value of inner is 0 The value of inner is 1 Breaking out of inner loop The value of outer is 2 The value of inner is 0 The value of inner is 1 Breaking out of inner loop Breaking out of outer loop Exiting program لاحظ أنه في كل مرة تنكسر فيها الحلقة الداخلية، فإن الحلقة الخارجية لا تنكسر لأن تعليمة break ستكسِر فقط تنفيذ الحلقة الداخلية التي استُدعيَت منها. الخاتمة رأينا في هذا المقال كيف تعمل حلقة التكرار for في لغة جو وكيف نستطيع إنشاءها واستعمالها، حيث تستمر حلقة for بتنفيذ مجموعة من الشيفرات لعدد محدد من المرات، كما تعرّفنا أيضًا على ثلاثة أنواع مختلفة من هذه الحلقة وكيفية استخدام كل منها، وتعلمنا أيضًا كيفية استخدام تعليمتَي continue و break للتحكم باستخدام حلقة for بفاعلية أكبر. ترجمة -وبتصرُّف- للمقالَين How To Construct For Loops in Go و Using Break and Continue Statements When Working with Loops in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: التعامل مع تعليمة التبديل Switch في لغة جو Go الحلقات التكرارية في البرمجة ما هي البرمجة ومتطلبات تعلمها؟
  19. تمنح التعليمات الشرطية المبرمجين القدرة على التحكم بسير عمل برامجهم وتفريعها وتوجيهها لاتخاذ بعض الإجراءات إذا كان الشرط المحدَّد صحيحًا وإجراء آخر إذا كان الشرط خاطئًا. تتوفَّر عدة تعليمات للتفريع branching statements بلغة جو، فقد تناولنا تعليمة if و else و else if والتعليمات الشرطية المتداخلة في المقال السابق كيفية كتابة التعليمات الشرطية if في لغة جو Go، وسننتقل الآن إلى تعليمة تفريع أخرى تدعى switch والتي يُعدّ استخدامها أقل شيوعًا من التعليمات السابقة، ولكن مع ذلك فهي مفيدة أحيانًا للتعبير عن نوع معيّن من التفريع المُتعدِّد multiway branches عندما نريد مثلًا مقارنة قيمة متغير (أو دخل من المستخدِم) بعدة قيم محتملة واتخاذ إجراء محدد بناءً على ذلك. إن أفضل مثال عملي على ذلك هو برنامج الآلة الحاسبة الذي يُجد ناتج عملية حسابية لعددين، بحيث يكون الدخل هو عملية حسابية -وهذا يكافئ عدة قيم محتملة + أو - أو * أو /- وعددين x و y.، وسيعتمد هنا الإجراء المُتخذ على نوع العملية التي يُدخلها المستخدِم، فإذا أدخل + فسيكون الإجراء x+y، وإذا أدخل - فسيكون x-y، وهكذا. يمكن تحقيق التفريع المتعدد باستخدام التعليمات الشرطية التي تعرفنا عليها في المقال السابق، لكن يمكن تحقيقه أيضًا باستخدام تعليمة switch كما سنرى في هذا المقال. بنية التعليمة Switch عادةً ما تُستخدَم تعليمة التبديل Switch لوصف الحالات التي يكون لدينا فيها عدة إجراءات ممكنة تبعًا لقيم متغير أو عدة متغيرات مُحتمَلة، ويوضح المثال التالي كيف يمكننا تحقيق ذلك باستخدام تعليمات if: package main import "fmt" func main() { flavors := []string{"chocolate", "vanilla", "strawberry", "banana"} for _, flav := range flavors { if flav == "strawberry" { fmt.Println(flav, "is my favorite!") continue } if flav == "vanilla" { fmt.Println(flav, "is great!") continue } if flav == "chocolate" { fmt.Println(flav, "is great!") continue } fmt.Println("I've never tried", flav, "before") } } سيكون الخرج كما يلي: chocolate is great! vanilla is great! strawberry is my favorite! I've never tried banana before نُعرّف بداخل الدالة main شريحةً slice من نكهات المثلجات ثم نستخدِم حلقة for للتكرار على عناصرها ثم نستخدِم ثلاث تعليمات if لطباعة رسائل مختلفة تشير إلى تفضيلات نكهات المثلجات المختلفة، إذ يجب على كل تعليمة if أن تتضمن تعليمة Continue لإيقاف التكرار الحالي في الحلقة لكي لا تُطبَع الرسالة الافتراضية في نهاية كتلة الحلقة. لاحظ أنه كلما أضفنا نكهةً جديدةً إلى الشريحة السابقة، فسيتعين علينا إضافة التفضيل المقابل لها، وبالتالي الاستمرار في كتابة تعليمات if في كل مرة نضيف فيها عنصرًا جديدًا إلى الشريحة، كما يجب أيضًا تكرار تعليمات if مع الرسائل المكررة كما في حالة "Vanilla" و "chocolate". كما ترى فإن تكرار تعليمات if باستمرار ووجود رسالة افتراضية في نهاية كتلة الحلقة بدون تعليمة if يجعل الأمر غريبًا وغير مرتب تمامًا، ولاحظ أيضًا أنّ كل ما يحدث هو مقارنة المتغير بقيم متعددة واتخاذ إجراءات مختلفة بناءً على ذلك، وهنا تكون بنية Switch قادرةً على التعبير عمّا يحدث بطريقة أفضل وأكثر تنظيمًا ورتابةً. تبدأ تعليمة التبديل بالكلمة المفتاحية switch متبوعةً بمتغير أو عدة متغيرات لإجراء مقارنات على أساسها (أبسط شكل) متبوعةً بقوسين معقوصَين توضَع ضمنها الحالات المتعددة التي يمكن أن يحملها المتغير أو المتغيرات، وكل من هذه الحالات تبدأ بالكلمة المفتاحية case متبوعةً بقيمة محتملة للمتغير، بحيث تقابل كل حالة إجراءً مُحددًا يُنفَّذ في حال كانت قيمة المتغير تطابق القيمة المحتملة، وسيبدو الأمر أوضح في المثال التالي الذي يكافئ المثال السابق تمامًا: package main import "fmt" func main() { flavors := []string{"chocolate", "vanilla", "strawberry", "banana"} for _, flav := range flavors { switch flav { case "strawberry": fmt.Println(flav, "is my favorite!") case "vanilla", "chocolate": fmt.Println(flav, "is great!") default: fmt.Println("I've never tried", flav, "before") } } } يكون الخرج كما يلي: chocolate is great! vanilla is great! strawberry is my favorite! I've never tried banana before نُعرّف بداخل الدالة main كما في المرة السابقة شريحةً slice من نكهات المثلجات ثم نستخدِم حلقة for للتكرار على عناصرها، لكن سنستخدِم في هذه المرة تعليمة التبديل وسنلغي استخدام التعليمة if، إذ وضعنا المتغير flav بعد الكلمة switch وستُختبر قيمة هذا المتغير وستُنفّذ إجراءات محددة بناءً عليها: الحالة الأولى سيطبع رسالةً محددةً عندما تكون قيمة هذا المتغير تساوي strawberry. الحالة الثانية سيطبع رسالةً محددةً عندما تكون قيمة هذا المتغير تساوي vanilla أو chocolate، ولاحظ أنه في المثال السابق اضطررنا لكتابة تعليمتَي if. الحالة الأخيرة هي الحالة الافتراضية وتسمى default حيث أنه إذا لم تتحقق أيّ من الحالات السابقة، فستُنفّذ هذه التعليمة ويطبع الرسالة المحددة، لكن إذا تحققت حالة ما من الحالات السابقة، فلن تُنفّذ هذه التعليمة إطلاقًا. ملاحظة: لم نحتاج إلى استخدام continue لأن تعليمة التبديل لا تُنفّذ إلا حالةً واحدةً فقط تلقائيًا. يعرض هذا المثال الاستخدام الأكثر شيوعًا لتعليمة التبديل وهو مقارنة قيمة متغير بعدة قيم محتملة، إذ توفِّر لنا تعليمة التبديل الراحة عندما نريد اتخاذ الإجراء نفسه لعدة قيم مختلفة وتنفيذ إجراءات محددة عندما لا تُستوفَى أيّ من شروط الحالات المُعرَّفة. تعليمات التبديل العامة تُعَدّ تعليمة التبديل مفيدةً في تجميع مجموعات من الشروط الأكثر تعقيدًا لإظهار أنها مرتبطة بطريقة ما، وغالبًا ما يُستخدَم ذلك عند مقارنة متغير أو عدة متغيرات بمجال من القيم بدلًا من القيم المحددة كما في المثال السابق، ويُعَدّ المثال التالي تحقيقًا للعبة تخمين باستخدام تعليمات if التي يمكن أن تستفيد من تعليمة التبديل كما رأينا سابقًا: package main import ( "fmt" "math/rand" "time" ) func main() { rand.Seed(time.Now().UnixNano()) target := rand.Intn(100) for { var guess int fmt.Print("Enter a guess: ") _, err := fmt.Scanf("%d", &guess) if err != nil { fmt.Println("Invalid guess: err:", err) continue } if guess > target { fmt.Println("Too high!") continue } if guess < target { fmt.Println("Too low!") continue } fmt.Println("You win!") break } } سيختلف الخرج بناءً على العدد العشوائي المحدد ومدى جودة لعبك، وفيما يلي ناتج دورة لعب واحدة: Enter a guess: 10 Too low! Enter a guess: 15 Too low! Enter a guess: 18 Too high! Enter a guess: 17 You win! تحتاج لعبة التخمين إلى عدد عشوائي لمقارنة التخمينات به، لذا سنستخدِم الدالة rand.Intn من الحزمة math/rand لتوليده، كما سنستخدِم rand.Seed مولّد الأعداد العشوائية بناءً على الوقت الحالي لضمان حصولنا على قيم مختلفة للمتغير target في كل مرة نلعب فيها، ولاحظ أننا مررنا القيمة 100 إلى الدالة rand.Intn وبالتالي ستكون الأعداد المولَّدة ضمن المجال من 0 إلى 100، وأخيرًا سنستخدِم حلقة for لبدء جمع التخمينات من اللاعب. تعطينا الدالة fmt.Scanf إمكانية قراءة مدخلات المستخدِم وتخزينها في متغير من اختيارنا، وهنا نريد تخزين القيمة المُدخَلة في المتغير guess، كما نريد أن تكون قيمة هذا المتغير من النوع الصحيح int، لذا سنمرر لهذه الدالة العنصر النائب d% على أساس وسيط أول والذي يشير إلى وجود قيمة من النوع الصحيح، ويكون الوسيط الثاني هو عنوان المتغير guess الذي نريد حفظ القيمة المدخَلة به. نختبر بعد ذلك فيما إذا كان هناك دخل خاطئ من المستخدِم مثل إدخال نص بدلًا من عدد صحيح ثم نكتب تعليمتَي if بحيث تختبر الأولى فيما إذا كان التخمين الذي أدخله المستخدِم أكبر من قيمة العدد المولَّد ليطبع Too high!‎ وتختبر الثانية فيما إذا كان العدد أصغر ليطبع Too low!‎، وسيكون التخمين في الحالتين خاطئًا، أي القيمة المولّدة لا تتطابق مع التخمين وبالتالي يخسر، طبعًا لاننسى كتابة تعليمة continue بعد كل تعليمة if كما ذكرنا سابقًا، وأخيرًا إذا لم تتحقق أي من الشروط السابقة فإنه يطبع You win!‎ دلالةً إلى أنّ تخمينه صحيح وتتوقف الحلقة لوجود تعليمة break؛ أما إذا لم يكن تخمينه صحيح، فستتكرر الحلقة مرةً أخرى. لاحظ أنّ استخدام تعليمات if يحجب حقيقة أن مجال القيم التي يُقارن معها المتغير مرتبطة كلها بطريقة ما، كما أنه قد يكون من الصعب معرفة ما إذا كنا قد فاتنا جزء من المجال، وفي المثال التالي سنعدّل المثال السابق باستخدام تعليمة التبديل: package main import ( "fmt" "math/rand" ) func main() { target := rand.Intn(100) for { var guess int fmt.Print("Enter a guess: ") _, err := fmt.Scanf("%d", &guess) if err != nil { fmt.Println("Invalid guess: err:", err) continue } switch { case guess > target: fmt.Println("Too high!") case guess < target: fmt.Println("Too low!") default: fmt.Println("You win!") return } } } سيكون الخرج مُشابهًا لما يلي: Enter a guess: 25 Too low! Enter a guess: 28 Too high! Enter a guess: 27 You win! استبدلنا هنا كل تعليمات if بتعليمة التبديل switch، ولاحظ أننا لم نذكر بعد تعليمة التبديل أيّ متغير كما فعلنا في أول مثال من المقال لأن هدفنا هو تجميع الشروط معًا كما ذكرنا، وتتضمن تعليمة التبديل في هذا المثال ثلاث حالات: الحالة الأولى عندما تكون قيمة المتغير guess أكبر من target. الحالة الثانية عندما تكون قيمة المتغير guess أصغر من target. الحالة الأخيرة هي الحالة الافتراضية default، حيث أنه إذا لم تتحقق أي من الحالات السابقة أي أن guess يساوي target ستُنفَّذ هذه التعليمة ويطبع الرسالة المُحددة، لكن إذا تحققت حالة ما من الحالات السابقة فلن تُنفّذ هذه التعليمة إطلاقًا، إذًا تُشير هذه الحالة إلى أن التخمين يطابق القيمة المولّدة. ملاحظة: لا حاجة إلى استخدام continue لأن تعليمة التبديل لاتُنفّذ إلا حالة واحدة فقط تلقائيًا. نلاحظ في الأمثلة السابقة أن حالة واحدة ستُنفّذ، لكن قد تحتاج أحيانًا إلى دمج سلوك عدة حالات معًا، لذا توفِّر تعليمة التبديل كلمةً مفتاحيةً أخرى لإنجاز هذا السلوك. التعليمة fallthrough: نفذ الخطوة التالية أيضا ربما ترغب بإعادة تنفيذ الشيفرة الموجودة ضمن حالة أخرى، وهنا يمكنك الطلب من جو أن يُشغّل الشيفرة التي تتضمنها الحالة التالية من خلال وضع التعليمة fallthrough في نهاية شيفرة الحالة الحالية، وفي المثال التالي سنُعدِّل المثال الذي عرضناه في بداية المقال والمتعلق بنكهات المثلجات لكي نوضّح كيف يمكننا استخدام هذه التعليمة فيه: package main import "fmt" func main() { flavors := []string{"chocolate", "vanilla", "strawberry", "banana"} for _, flav := range flavors { switch flav { case "strawberry": fmt.Println(flav, "is my favorite!") fallthrough case "vanilla", "chocolate": fmt.Println(flav, "is great!") default: fmt.Println("I've never tried", flav, "before") } } } سيكون الخرج كما يلي: chocolate is great! vanilla is great! strawberry is my favorite! strawberry is great! I've never tried banana before نُعرّف داخل الدالة main شريحةً slice من نكهات المثلجات ثم نستخدِم حلقة for للتكرار على عناصرها ثم نستخدِم تعليمة التبديل ونضع المتغير flav بعد الكلمة switch بحيث تُختبر قيمته وتُنفَّذ إجراءات محددة بناءً عليها: الحالة الأولى سيطبع رسالةً محددةً عندما تكون قيمة هذا المتغير تساوي vanilla أو chocolate. الحالة الثانية سيطبع strawberry is my favorite!‎ عندما تكون قيمة هذا المتغير تساوي strawberry ثم سيُنفّذ التعليمة fallthrough التي تؤدي إلى تنفيذ شيفرة الحالة التالية لها، وبالتالي سيطبع strawberry is great!‎ أيضًا. الحالة الأخيرة هي الحالة الافتراضية default. لا يستخدِم المطورون تعليمة fallthrough في لغة جو كثيرًا لأنه من الممكن الاستغناء عنها بتعريف دالة تؤدي الغرض واستدعائها ببساطة، وعمومًا لا يُنصَح باستخدامها. الخاتمة تساعدنا تعليمة التبديل في التعبير عن ارتباط مجموعة من عمليات المقارنة ببعضها بطريقة ما، وهذا مهم لجعل المطورين الذين يقرؤون الشيفرة يفهمون هذا الارتباط، كما أنها تُسهّل عملية إضافة سلوك جديد وحالات جديدة لاحقًا عندما تدعو الحاجة إلى ذلك وتضمن لنا أنه دومًا سيكون هناك تعليمة افتراضية تُنفَّذ في حال نسينا كتابة حالة من الحالات أو عند عدم تحقق أية حالة من الحالات، لذا جرّب استخدام تعليمة التبديل بدلًا عنها عندما تحتاج إلى كتابة عدة تعليمات if في المرات القادمة، وستجد أنها أسهل بكثير وأكثر قابليةً لإعادة الاستخدام والتصحيح والتعديل. ترجمة -وبتصرف- للمقال How To Construct For Loops in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: كيفية كتابة التعليمات الشرطية if في لغة جو Go كتابة برنامجك الأول في جو Go مقدمة في البرمجة الشرطية
  20. لا تخلو أية لغة برمجية من التعليمات الشرطية Conditional Statements التي تُنفَّذ بناءً على تحقق شرط معيّن، إذ تُعَدّ تعليمات برمجية يمكنها التحكم في تنفيذ شفرات معينة بحسب تحقق شرط ما من عدمه في وقت التنفيذ، وباستخدام التعليمات الشرطية يمكن للبرامج التحقق من استيفاء شروط معينة ومن ثم تنفيذ الشيفرة المقابلة. تُنفّذ التعليمات البرمجية في الحالة الطبيعية سطرًا سطرًا ومن الأعلى إلى الأسفل؛ أما باستخدام التعليمات الشرطية، فيمكن للبرامج التحقق من استيفاء شروط معينة ومن ثم تنفيذ الشيفرة المقابلة وكسر تسلسل التنفيذ هذا أو تجاوز كتلة من التعليمات، أي باختصار تمكننا التعليمات الشرطية من التحكم بسير عمل البرنامج، وهذه بعض الأمثلة التي سنستخدِم فيها التعليمات الشرطية: إذا حصل الطالب على أكثر من 65% في الامتحان، فأعلن عن نجاحه؛ وإلا، فأعلن عن رسوبه. إذا كان لديه مال في حسابه، فاطقع منه قيمة الفاتورة، وإلا، فاحسب غرامة. إذا اشتروا 10 برتقالات أو أكثر، فاحسب خصمًا بمقدار 5%؛ وإلا، فلا تفعل. تقيِّم الشيفرة الشرطية شروطًا ثم تُنفِّذ شيفرةً بناءً على ما إذا تحققت تلك الشروط أم لا، وستتعلم في هذا المقال كيفية كتابة التعليمات الشرطية في لغة جو. التعليمة if سنبدأ بالتعليمة ‎if‎ والتي تتحقق مما إذا كان شرطًا محدَّدًا محقّقًا true أم لا false، ففي حال تحقق الشرط، فستُنفَّذ الشيفرة المقابلة، وإلا فسيتابع البرنامج في مساره الطبيعي، ولنبدأ بأمثلة عملية توضح ذلك، لذا افتح ملفًا واكتب الشيفرة التالية: package main import "fmt" func main() { grade := 70 if grade >= 65 { fmt.Println("Passing grade") } } أعطينا للمتغير ‎grade‎ القيمة الصحيحة ‎70‎ ثم استخدمنا التعليمة ‎if‎ لتقييم ما إذا كان أكبر من أو يساوي ‎65‎ وفي تلك الحالة سيطبع البرنامج السلسلة النصية التالية: Passing grade. احفظ البرنامج بالاسم ‎grade.go‎ ثم نفّذه في بيئة البرمجة المحلية من نافذة الطرفية باستخدام الأمر ‎go run grade.go، وفي هذه الحالة تلبي الدرجة 70 تلبي لأنها أكبر من 65، لذلك ستحصل على الخرج التالي عند تنفيذ البرنامج: Passing grade لنغيّر الآن نتيجة هذا البرنامج عبر تغيير قيمة المتغير ‎grade‎ إلى ‎60‎: package main import "fmt" func main() { grade := 60 if grade >= 65 { fmt.Println("Passing grade") } } لن نحصل على أية خرج بعد حفظ وتنفيذ الشيفرة لأنّ الشرط لم يتحقق ولم نأمر البرنامج بتنفيذ تعليمة أخرى. لنأخذ مثالًا آخرًا، إذ نريد التحقق مما إذا كان رصيد الحساب المصرفي أقل من 0، لذا سننشئ ملفًا باسم ‎account.go‎ ونكتب البرنامج التالي: package main import "fmt" func main() { balance := -5 if balance < 0 { fmt.Println("Balance is below 0, add funds now or you will be charged a penalty.") } } سنحصل على الخرج التالي عند تنفيذ البرنامج باستخدام الأمر go run account.go‎‎: Balance is below 0, add funds now or you will be charged a penalty. أعطينا للمتغير ‎balance‎ القيمة ‎-5‎ وهي أقل من 0 في البرنامج السابق، ولمَّا كان الرصيد مستوفيًا لشرط التعليمة ‎if‎ أي ‎balance < 0‎، فسنحصل على سلسلة نصية في الخرج بمجرد حفظ الشيفرة وتنفيذها، وإذا غيّرنا الرصيد إلى القيمة 0 أو إلى عدد موجب مرةً أخرى، فلن نحصل على أيّ خرج. التعليمة else قد تريد من البرنامج فعل شيء ما في حال عدم تحقق شرط التعليمة ‎if‎، ففي المثال أعلاه نريد طباعة خرج في حال النجاح والرسوب، ولفعل ذلك سنضيف التعليمة ‎else‎ إلى شرط الدرجة أعلاه وفق الصياغة التالية: package main import "fmt" func main() { grade := 60 if grade >= 65 { fmt.Println("Passing grade") } else { fmt.Println("Failing grade") } } تساوي قيمة المتغير ‎‎grade‎‎ العدد ‎60‎، لذلك فشرط التعليمة ‎if‎ غير متحقق، وبالتالي لن يطبع البرنامج ‎درجة النجاح‎، إذ تخبر التعليمة ‎else‎ البرنامج أنه عليه طباعة السلسلة النصية Failing grade، لذا عندما نحفظ البرنامج وننفّذه، فسنحصل على الخرج التالي: Failing grade إذا عدّلنا البرنامج وأعطينا المتغير grade القيمة ‎65‎ أو أعلى منها، فسنحصل بدلًا من ذلك على Passing grade، ولإضافة التعليمة ‎else‎ إلى مثال الحساب المصرفي، سنعيد كتابة الشيفرة كما يلي: package main import "fmt" func main() { balance := 522 if balance < 0 { fmt.Println("Balance is below 0, add funds now or you will be charged a penalty.") } else { fmt.Println("Your balance is 0 or above.") } } سنحصل على الخرج التالي: Your balance is 0 or above. غيّرنا هنا قيمة المتغير ‎balance‎ إلى عدد موجب لكي تُنفَّذ الشيفرة المقابلة للتعليمة ‎else‎، فإذا أردت تنفيذ الشيفرة المقابلة للتعليمة ‎if‎، فغيِّر القيمة إلى عدد سالب. من خلال دمج العبارتين ‎if‎ و ‎else‎، فأنت تنشئ تعليمة شرطية مزدوجة والتي ستجعل الحاسوب ينفذ شيفرة برمجية معينّة سواءً تحقق شرط ‎if‎ أم لا. التعليمة else if عملنا حتى الآن على تعليمات شرطية ثنائية، أي إذا تحقق الشرط، فنفِّذ شيفرةً ما، وإلا، فنفِّذ شيفرةً أخرى فقط، لكن قد تريد في بعض الحالات برنامجًا يتحقق من عدة حالات شرطية، ولأجل هذا سنستخدِم التعليمة Else if والتي تُكتب في جو بالصورة else if، إذ تشبه هذه التعليمة تعليمة ‎if‎ ومهمتها التحقق من شرط إضافي. قد نرغب في برنامج الحساب المصرفي في الحصول على ثلاثة مخرجات مختلفة مقابلة لثلاث حالات مختلفة: الرصيد أقل من 0. الرصيد يساوي 0. الرصيد أعلى من 0. لذا ستوضع التعليمة else if بين التعليمة ‎if‎ والتعليمة ‎else‎ كما يلي: package main import "fmt" func main() { balance := 522 if balance < 0 { fmt.Println("Balance is below 0, add funds now or you will be charged a penalty.") } else if balance == 0 { fmt.Println("Balance is equal to 0, add funds soon.") } else { fmt.Println("Your balance is 0 or above.") } } هناك الآن ثلاثة مخرجات محتملة يمكن أن تُطبع عند تنفيذ البرنامج وهي: إذا كان المتغير ‎balance‎ يساوي ‎0‎، فسنحصل على الخرج من التعليمة else if‎ (أي ‎الرصيد يساوي 0، أضف مبلغًا قريبًا‎). إذا ضُبِط المتغير ‎balance‎على عدد موجب، فسنحصل على المخرجات من التعليمة ‎else‎ (أي ‎رصيدك أكبر من 0). إذا ضُبِط المتغير ‎balance‎على عدد سالب، فسنحصل على المخرجات من التعليمة ‎if‎ (أي ‎الحساب فارغ، أضف مبلغا الآن أو ستحصل على غرامة‎). إذا أردنا أن نأخذ بالحسبان أكثر من ثلاثة احتمالات، فيمكننا كتابة عدة تعليمات else if‎ في الشيفرة البرمجية، ولنُعِد الآن كتابة البرنامج ‎grade.go‎ بحيث يقابل كل نطاق من الدرجات العددية درجة حرفية محددة: الدرجة 90 أو أعلى تكافئ الدرجة A. من 80 إلى 89 تكافئ الدرجة B. من 70 إلى 79 تكافئ الدرجة C. من 65 إلى 69 تكافئ الدرجة D. الدرجة 64 أو أقل تكافئ الدرجة F. سنحتاج لتنفيذ هذه الشيفرة إلى تعليمة ‎if‎ واحدة وثلاث تعليمات else if‎ وتعليمة ‎else‎ تعالج جميع الحالات الأخرى، لذا دعنا نعيد كتابة الشيفرة من المثال أعلاه لطباعة سلسلة نصية مقابلة لكل علامة، إذ يمكننا الإبقاء على التعليمة ‎else‎ كما هي. package main import "fmt" func main() { grade := 60 if grade >= 90 { fmt.Println("A grade") } else if grade >= 80 { fmt.Println("B grade") } else if grade >= 70 { fmt.Println("C grade") } else if grade >= 65 { fmt.Println("D grade") } else { fmt.Println("Failing grade") } } تُنفّذ تعليمات else if‎ بالترتيب، وسيكمل هذا البرنامج الخطوات التالية: إذا كانت الدرجة أكبر من 90، فسيطبع البرنامج الدرجة A، وإذا كانت الدرجة أقل من 90، فسيستمر البرنامج إلى التعليمة التالية. إذا كانت الدرجة أكبر من أو تساوي 80، فسيطبع البرنامج الدرجة B‎، إذا كانت الدرجة تساوي 79 أو أقل، فسيستمر البرنامج إلى التعليمة التالية. إذا كانت الدرجة أكبر من أو تساوي 70، فسيطبعُ البرنامجُ الدرجة C، إذا كانت الدرجة تساوي 69 أو أقل، فسيستمر البرنامج إلى التعليمة التالية. إذا كانت الدرجة أكبر من أو تساوي 65، فسيطبع البرنامج الدرجة D، وإذا كانت الدرجة تساوي 64 أو أقل، فسيستمر البرنامج إلى التعليمة التالية. سيطبع البرنامج ‎Failing grade لأنه لم تستوفى أي من الشروط المذكورة أعلاه. تعليمات if المتداخلة يمكنك الانتقال إلى التعليمات الشرطية المتداخلة بعد أن تتعود على التعليمات ‎if‎ و else if‎ و ‎else، إذ يمكننا استخدام تعليمات ‎if‎ المتداخلة في الحالات التي نريد فيها التحقق من شرط ثانوي بعد التأكد من تحقق الشرط الرئيسي، وبالتالي يمكننا حشر تعليمة if-else داخل تعليمة if-else أخرى، ولنلقِ نظرةً على صياغة ‎if‎ المتداخلة: if statement1 { // الخارجية if تعليمة fmt.Println("true") if nested_statement { // المتداخلة if تعليمة fmt.Println("yes") } else { // المتداخلة else تعليمة fmt.Println("no") } } else { // الخارجية else تعليمة fmt.Println("false") } هناك عدة مخرجات محتملة لهذه الشيفرة: إذا كانت التعليمة ‎statement1‎ صحيحةً، فسيتحقق البرنامج مما إذا كانت ‎nested_statement‎ صحيحةً أيضًا، فإذا كانت كلتا الحالتين صحيحتين، فسنحصل على المخرجات التالية: true yes ولكن إذا كانت ‎statement1‎ صحيحةً و ‎nested_statement‎ خاطئةً، فسنحصل على المخرجات التالية: true no وإذا كانت ‎statement1‎ خاطئةً، فلن تُنفّذ تعليمة if-else المتداخلة على أيّ حال، لذلك ستُنفّذ التعليمة ‎else‎ وحدها وستكون المخرجات كما يلي: false يمكن أيضًا استخدام عدة تعليمات ‎if‎ متداخلة في الشيفرة: if statement1 { // الخارجية if fmt.Println("hello world") if nested_statement1 { // المتداخلة الأولى if fmt.Println("yes") } else if nested_statement2 { // المتداخلة الأولى else if fmt.Println("maybe") } else { // المتداخلة الأولى else fmt.Println("no") } } else if statement2 { // الخارجية else if fmt.Println("hello galaxy") if nested_statement3 { // المتداخلة الثانية if fmt.Println("yes") } else if nested_statement4 { // المتداخلة الثانية else if fmt.Println("maybe") } else { // المتداخلة الثانية else fmt.Println("no") } } else { // الخارجية else statement("hello universe") } توجد في الشيفرة البرمجية أعلاه تعليمات ‎if‎ و else if متداخلة داخل كل تعليمات ‎if‎، إذ سيفسح هذا مجالًا لمزيد من الخيارات في كل حالة. دعنا نلقي نظرةً على مثال لتعليمات ‎if‎ متداخلة في البرنامج ‎grade.go، إذ يمكننا التحقق أولًا مما إذا كان الطالب قد حقق درجة النجاح (أكبر من أو تساوي 65%) ثم نحدد العلامة المقابلة للدرجة، فإذا لم يحقق الطالب درجة النجاح، فلا داعي للبحث عن العلامة المقابلة للدرجة، وبدلًا من ذلك، يمكن أن نجعل البرنامج يطبع سلسلة نصية فيها إعلان عن رسوب الطالب، وبالتالي ستبدو الشيفرة المعدلة كما يلي: package main import "fmt" func main() { grade := 92 if grade >= 65 { fmt.Print("Passing grade of: ") if grade >= 90 { fmt.Println("A") } else if grade >= 80 { fmt.Println("B") } else if grade >= 70 { fmt.Println("C") } else if grade >= 65 { fmt.Println("D") } } else { fmt.Println("Failing grade") } } إذا أعطينا للمتغير ‎grade‎ القيمة ‎92‎، فسيتحقق الشرط الأول وسيطبع البرنامج العبارة "‎Passing grade of:‎"، بعد ذلك سيتحقق مما إذا كانت الدرجة أكبر من أو تساوي 90، وبما أنّ هذا الشرط محقق أيضًا، فستُطبع ‎A‎؛ أما إذا أعطينا للمتغير ‎grade‎ القيمة ‎60‎، فلن يتحقق الشرط الأول، لذلك سيتخطى البرنامج تعليمات ‎if‎ المتداخلة وينتقل إلى التعليمة ‎else‎ ويطبع ‎Failing grade. يمكننا بالطبع إضافة المزيد من الخيارات واستخدام طبقة ثانية من تعليمات ‎if‎ المتداخلة، فربما نودّ إضافة الدرجات التفصيلية A+‎ و A و A-‎، إذ يمكننا إجراء ذلك عن طريق التحقق أولًا من اجتياز درجة النجاح ثم التحقق مما إذا كانت الدرجة تساوي 90 أو أعلى ثم التحقق مما إذا كانت الدرجة تتجاوز 96، وفي تلك الحالة ستقابل العلامة A+، وإليك المثال التالي: ... if grade >= 65 { fmt.Print("Passing grade of: ") if grade >= 90 { if grade > 96 { fmt.Println("A+") } else if grade > 93 && grade <= 96 { fmt.Println("A") } else { fmt.Println("A-") } ... سيؤدي البرنامج السابق ما يلي في حال تعيين المتغير ‎grade‎ على القيمة ‎96‎: التحقق مما إذا كانت الدرجة أكبر من أو تساوي 65 (صحيح). طباعة ‎Passing grade of:‎ التحقق مما إذا كانت الدرجة أكبر من أو تساوي 90 (صحيح). التحقق مما إذا كانت الدرجة أكبر من 96 (خطأ). التحقق مما إذا كانت الدرجة أكبر من 93 وأقل من أو تساوي 96 (صحيح). طباعة A. تجاوز التعليمات الشرطية المتداخلة وتنفيذ باقي الشيفرة. سيكون خرج البرنامج إذا كانت الدرجة تساوي 96 كما يلي: Passing grade of: A تساعد تعليمات ‎if‎ المتداخلة على إضافة عدة مستويات من الشروط الفرعية إلى الشيفرة. الخاتمة ستتحكم باستخدام التعليمات الشرطية في مسار عمل البرنامج أي تدفق تنفيذ الشيفرة، إذ تطلب التعليمات الشرطية من البرنامج التحقق من استيفاء شرط معيّن من عدمه، فإذا استُوفي الشرط، فستُنفّذ شيفرة معينة، وإلا، فسيستمر البرنامج وينتقل إلى الأسطر التالية. ترجمة -وبتصرف- للمقال How To Write Conditional Statements in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: فهم مجال رؤية الحزم Visibility في لغة جو Go مقدمة في البرمجة الشرطية تعلم أساسيات البرمجة
  21. الهدف من إنشاء الحزم في لغة جو أو في أي لغة أخرى هو جعل هذه الحزمة متاحة للاستخدام وسهلة الوصول في أيّ وقت من قِبَل مطورين آخرين أو حتى نحن، إذ تُستخدَم هذه الحزم ضمن برمجيات محددة أو بوصفها جزءًا من حزم أكثر تعقيدًا أو أعلى مستوى، لكن لايمكن الوصول إلى جميع الحزم من خارج الحزمة نفسها، ويعتمد ذلك على نطاق رؤيتها. تُعَدّ الرؤية Visibility هي النطاق الذي يمكن الوصول للحزمة ضمنه، فإذا صرّحت مثلًا عن متغير داخل دالة ضمن حزمة A، فلن تتمكن من الوصول إلى هذا المتغير إلى ضمن الدالة التي صُرّح عنه فيها وضمن الحزمة A فقط، في حين إذا صرّحت عن المتغير نفسه ضمن الحزمة وليس بداخل دالة أو أيّ نطاق آخر، فيمكنك السماح للحزم الأخرى بالوصول إلى هذا المتغير أو لا. عند التفكير بكتابة شيفرة مريحة ومرتّبة ومنظّمة لا بدّ من أن تكون دقيقًا في ضبط رؤية الحزم في مشروعك، ولاسيما عندما يكون من المحتمل تعديل ملفات مشروعك باستمرار، فعندما تحتاج إلى إجراء تعديلات مثل إصلاح زلة برمجية أو تحسين أداء أو تغيير دوال …إلخ بطريقة منظمة وبدون فوضى أو إرباك للمطورين الآخرين الذي يستخدمون الحزمة، فلا بدّ من أن تكون دقيقًا في تحديد الرؤية. تتمثّل إحدى طرق الضبط الدقيق لمثل هذه العمليات في تقييد الوصول -أي تقييد الرؤية-، بحيث تسمح بالوصول إلى أجزاء مُحددة فقط من الحزمة -أي الأجزاء القابلة للاستخدام فقط-، وبذلك تتمكّن من إجراء التغييرات الداخلية على حِزَمك مع تقليل احتمالية التأثير على استخدام المطورين للحزمة. ستتعلم في هذا المقال كيفية التحكم في رؤية الحزمة، وكذلك كيفية حماية أجزاء من التعليمات البرمجية الخاصة بك والتي يجب استخدامها فقط داخل حزمتك، إذ سننشئ أداة تسجيل لتسجيل الرسائل وتصحيح الأخطاء باستخدام حزم تتضمن عناصر لها درجات متفاوتة من الرؤية. المتطلبات أن يكون لديك مساحة عمل خاصة في لغة جو، فإذا لم يكن لديك، فاتبع سلسلة المقالات التالية، فقد تحدّثنا عن ذلك في بداية السلسلة تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu تثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS تثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز سيعتمد المقال على بنية المجلد التالية: . ├── bin │ └── src └── github.com └── gopherguides العناصر المصدرة وغير المصدرة لا تمتلك لغة جو محددات وصول لتحديد الرؤية مثل عام public وخاص private ومحمي protected كما في باقي لغات البرمجة، إذ تحدِّد لغة جو إمكانية رؤية العناصر اعتمادًا على ما إذا كان مُصدّرًا أم لا وذلك وفقًا لكيفية التصريح عنه، فإذا كان مصدّرًا فهو مرئي من خارج الحزمة وإلا فهو مرئي فقط داخل الحزمة التي أُنشئ فيها. لكي نجعل عنصرًا ما -مثل متغير أو دالة أو نوع بيانات جديد …إلخ- داخل الحزمة مُصدّرًا، سنكتب أول محرف منه كبيرًا، أي إذا كان اسم الدالة hsoub، فستكون مرئيةً فقط ضمن الحزمة نفسها وتكون غير مُصدّرة؛ أما إذا كتبناها Hsoub، فستكون مرئيةً من خارج الحزمة وتكون مُصدّرة. لاحظ المثال التالي وانتبه لحالة أول محرف في كل التصريحات: package greet import "fmt" var Greeting string func Hello(name string) string { return fmt.Sprintf(Greeting, name) } من الواضح أنّ هذه الشيفرة موجودة ضمن الحزمة greet، ولاحظ أننا صرّحنا عن متغير اسمه Greeting وجعلنا أول محرف كبيرًا، لذا فهذا المتغير سيكون مُصدّرًا ويمكن رؤيته من خارج الحزمة، وينطبق الأمر نفسه على الدالة Hell، فقد كتبنا أول محرف منها كبيرًا. كما ذكرنا سابقًا، إنّ تحديد إمكانية الوصول أو الرؤية يجعل الشيفرة منظمةً ويمنع حدوث فوضى أو إرباك للمطورين الآخرين الذي يستخدِمون الحزمة، كما يمنع أو يقلل من إمكانية حدوث تأثيرات سلبية على مشاريعهم التي تعتمد على حزمتك. تحديد رؤية الحزمة سنُنشئ الحزمة logging مع الأخذ بالحسبان ما نريده مرئيًا وما نريده غير مرئي لكي يكون لدينا نظرة أوضح عن آلية عمل قابلية الرؤية في برامجنا. ستكون هذه الحزمة مسؤولةً عن تسجيل أيّ رسالة من البرنامج إلى وحدة التحكم console، كما يُحدد المستوى الذي يحدث عنده التسجيل، إذ يصف المستوى نوع السجل وسيكون أحد الحالات الثلاث: معلومات info أو تحذير warning أو خطأ error، لذا أنشئ بدايةً مجلد الحزمة logging داخل المجلد src كما يلي: $ mkdir logging انتقل إلى مجلد الحزمة: $ cd logging أنشئ ملف logging.go باستخدام محرر شيفرات مثل نانو nano: $ nano logging.go ضع فيه الشيفرة التالية: package logging import ( "fmt" "time" ) var debug bool func Debug(b bool) { debug = b } func Log(statement string) { if !debug { return } fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement) } تصف التعليمة الأولى في هذه الشيفرة أننا ضمن الحزمة logging، وتوجد لدينا ضمن هذه الحزمة دالتين مُصدّرتين هما Debug و Log، وبالتالي يمكن استدعاء هذه الدوال من أيّ حزمة أخرى تستورد الحزمة logging، ولدينا أيضًا متغير غير مُصدّر اسمه debug، وبالتالي لايمكن الوصول إليه إلى ضمن الحزمة. على الرغم من أنه لدينا متغير ودالة بالاسم نفسه "debug"، إلا أنهما مختلفان، فالدالة بدأت بمحرف كبير؛ أما المتغير، فقد بدأ بمحرف صغير ولغة جو حساسة لحالة المحارف. احفظ الملف بعد وضع الشيفرة فيه، ولاستخدام الحزمة في مكان آخر، يجب استيرادها، لذا سنُنشئ حزمةً جديدةً ونجرب عليها، وبالتالي انتقل إلى المجلد logging وأنشئ مجلدًا اسمه cmd وانتقل إليه: $ cd .. $ mkdir cmd $ cd cmd أنشئ الملف main.go بداخله: $ nano main.go أضف إليه الشيفرة التالية: package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") } أصبح لدينا الآن كامل الشيفرة المطلوب، لكن نحتاج قبل تشغيلها إلى إنشاء ملفَي ضبط حتى تعمل التعليمات البرمجية بطريقة صحيحة. تُستخدَم وحدات لغة جو Go Modules لضبط اعتماديات الحزمة لاستيراد الموارد، إذ تُعَدّ وحدات لغة جو ملفات ضبط موضوعة في مجلد الحزمة الخاص بك وتخبر المُصرّف بمكان استيراد الحزم منه، ولن نتحدث عن وحدات لغة جو في هذا المقال، لكن سنكتب السطرين التاليين لكي تعمل الشيفرة السابقة، لذا افتح ملف go.mod ضمن المجلد cmd: $ nano go.mod ثم ضع السطرين التاليين فيه: module github.com/gopherguides/cmd replace github.com/gopherguides/logging => ../logging يُخبر السطر الأول المُصرّف أنّ الحزمة cmd لديها مسار الملف github.com/gopherguides/cmd؛ أما السطر الثاني، فيُخبر المُصرّف أنّ الحزمة github.com/gopherguides/logging يمكن العثور عليها محليًا على القرص في المجلد ‎../logging. نحتاج إيضًا إلى وجود ملف go.mod بداخل الحزمة logging، لذا سننتقل إلى مجلدها ثم سننشئ هذا الملف بداخله: $ cd ../logging $ nano go.mod أضف الشيفرة التالية إلى الملف: module github.com/gopherguides/logging يخبر هذا المترجم أنّ حزمة logging التي أنشأناها هي الحزمة github.com/gopherguides/logging، إذ سيسمح لنا ذلك باستيراد الحزمة من داخل الحزمة main كما يلي: package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") } يجب أن يكون لديك الآن بنية المجلد التالية: ├── cmd │ ├── go.mod │ └── main.go └── logging ├── go.mod └── logging.go أصبح بإمكاننا الآن تشغيل البرنامج main من حزمة cmd بالأوامر التالية بعد أن أنشأنا جميع ملفات الضبط: $ cd ../cmd $ go run main.go سنحصل على رسالة تطبع التوقيت وتقول أن هذه تعليمة تنقيح debug: 2019-08-28T11:36:09-05:00 This is a debug statement... يطبع البرنامج الوقت الحالي بتنسيق RFC 3339 متبوعًا بالتعليمة التي أرسلناها إلى المُسجّل logger، وقد صُمِّم RFC 3339 لتمثيل الوقت على الإنترنت ومن الشائع استخدامه في ملفات التسجيل والتتبع log files. بما أن الدالتين Debug و Log هما دوال مُصدّرة، لذا يمكن استخدامهما في الحزمة main، لكن لا يمكن استخدام المتغير debug في حزمة logging، وإذا حاولت الوصول إليه، فستحصل على خطأ في وقت التصريف compile-time error. سنضيف السطر (fmt.Println(logging.debug إلى main.go: package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") fmt.Println(logging.debug) } احفظ وشغل الملف، إذ ستحصل على الخطأ التالي (لا يمكن الإشارة إلى العنصر debug لأنه غير مُصدّر): . . . ./main.go:10:14: cannot refer to unexported name logging.debug سنتعرّف الآن على كيفية تصدير الحقول والتوابع من داخل السجلات structs بعد أن تعرّفنا على الملفات المُصدّرة وغير المُصدّرة. نطاق الرؤية داخل السجلات structs تحدّثنا في الأمثلة السابقة عن إمكانية الوصول إلى عنصر ينتمي إلى حزمة مُحددة من حزمة أخرى، كذلك رأينا أنه يمكننا تعديل العناصر التي تكون مُصدّرة. غالبًا ستسير الأمور على مايُرام، لكن في بعض الحالات قد تظهر لدينا بعض المشاكل. مثلًا، عندما تستخدم أكثر من حزمة متغير وتعدّل إحداها على قيمته، فسيكون التعديل على النسخة نفسها والتي ستنعكس على كل تلك الحزم التي تصل إليه، وبالتالي ستحدث لدينا مشكلة عدم اتساق في البيانات. تخيل أن تضبط المتغير Debug على true ويأتي شخص آخر لا تعرفه يستخدم الحزمة نفسها ويضبطه على false، لذا سنحل هذه المشكلة من خلال استخدام مفهوم النسخ الذي تتبناه السجلات Structs في جو، وبالتالي كل شخص يستخدِم هذه الحزمة سيأخذ نسخةً مستقلةً منها، وبالتالي سنتجنب مثل هذه المشاكل. سنعدّل الآن الحزمة logging كما يلي: package logging import ( "fmt" "time" ) type Logger struct { timeFormat string debug bool } func New(timeFormat string, debug bool) *Logger { return &Logger{ timeFormat: timeFormat, debug: debug, } } func (l *Logger) Log(s string) { if !l.debug { return } fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s) } أنشأنا هنا السجل Logger الذي سيضم العناصر غير المُصدّرة وتنسيق الوقت timeFormat المطلوب طباعته والمتغير debug وقيمته سواءً كانت true أو false. تضبط الدالة New الحالة الأولية لإنشاء السجل logger داخليًا ضمن المتغيران timeFormat و debug غير المُصدَّران، كما أنشأنا التابع Log بداخل هذا السجل الذي يأخذ المعلومات المراد طباعتها، كما يوجد بداخل التابع Log مرجع reference يعود إلى متغير الدالة المحلية l الخاص به للحصول على سماحية الوصول مرةً أخرى إلى الحقول الداخلية مثل l.timeFormat و l.debug. سيسمح لنا هذا النهج بإنشاء مسجل Logger في العديد من الحزم المختلفة واستخدامه باستقلال عن الحزم الأخرى له، ولاستخدامه في حزمة أخرى سنعدّل ملف cmd/main.go كما يلي: package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("This is a debug statement...") } بتشغيل البرنامج سنحصل على الخرج التالي: 2019-08-28T11:56:49-05:00 This is a debug statement... أنشأنا في هذا المثال نسخةً من المُسجّل logger من خلال استدعاء الدالة المُصدّرة New، كما خزّنا مرجعًا لهذه النسخة في متغير المُسجّل logger، ويمكننا الآن استدعاء logging.Log لطباعة المعلومات. إذا حاولنا الإشارة إلى حقول غير مُصدّرة من Logger مثل الحقل timeFormat، فسنحصل على خطأ في وقت التصريف، لذا جرّب إضافة السطر (fmt.Println(logger.timeFormat إلى الملف cmd/main.go: package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("This is a debug statement...") fmt.Println(logger.timeFormat) } ستحصل على الخطأ التالي (لا يمكن الإشارة إلى حقل أو دالة غير مصدرة): . . . cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat) سيُلاحظ المُصرّف أنّ logger.timeFormat غير مُصدّر، وبالتالي لا يمكن الوصول إليه من الحزمة logging. نطاق الرؤية في التوابع يمكننا تصدير أو عدم تصدير الدوال بطريقة الحقول ضمن السجلات Structs نفسها، ولتوضيح ذلك سنضيف مستويات التسجيل إلى المسجل logger الخاص بنا، إذ تُعَدّ مستويات التسجيل وسيلةً لتصنيف السجلات logs الخاصة بك، بحيث يمكنك البحث فيها عن أنواع معينة من الأحداث، وتكون المستويات التي سنضعها في المسجل logger كما يلي: مستوى المعلومات info: تمثِّل الأحداث التي تُعلِم المستخدِم بإجراء ما مثل بدء البرنامج Program started أو إرسال البريد الإلكتروني Email sent، كما تساعدنا في تصحيح الأخطاء وتتبع أجزاء من برنامجنا لمعرفة ما إذا كان السلوك المتوقع قد حدث أم لا. مستوى التحذير warning: تشير هذه الأحداث إلى حدوث أمر غير متوقع أو قد يُسبب مشاكل لاحقًا، لكنها ليست أخطاءً مثل فشل إرسال البريد الإلكتروني وإعادة محاولة الإرسالة Email failed to send, retrying، ويمكن القول أنها تساعدنا في رؤية أجزاء من برنامجنا لا تسير كما هو متوقع لها. مستوى الأخطاء error: تشير إلى حدوث أخطاء أو مشاكل في البرنامج مثل الملف غير موجودFile not found، فهذه الأحداث هي أخطاء تؤدي إلى فشل البرنامج وبالتالي إيقافه. قد ترغب في تفعيل أو إيقاف مستويات معينة من التسجيل، خاصةً إذا كان برنامجك لا يعمل كما هو متوقع له وترغب في تصحيح أخطاء البرنامج، لذا سنضيف وظيفة تعدِّل البرنامج بحيث عندما تكون debug مضبوطةً على true، فستُطبع كل رسائل المستويات؛ أما إذا كانت false، فستطبع رسائل الخطأ فقط. سنضيف مستويات التسجيل إلى الملف logging/logging.go: package logging import ( "fmt" "strings" "time" ) type Logger struct { timeFormat string debug bool } func New(timeFormat string, debug bool) *Logger { return &Logger{ timeFormat: timeFormat, debug: debug, } } func (l *Logger) Log(level string, s string) { level = strings.ToLower(level) switch level { case "info", "warning": if l.debug { l.write(level, s) } default: l.write(level, s) } } func (l *Logger) write(level string, s string) { fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s) } لاحظ أننا صرّحنا عن وسيط جديد للتابع Log هو level بغية تحديد مستوى التسجيل، فإذا كان لدينا رسالة معلومات info أو تحذير warning وكان حقل debug مضبوطًا على true، فسيكتب الرسالة، وإلا فسيتجاهلها؛ أما إذا كانت الرسالة هي رسالة خطأ error، فسيطبعها دومًا. يوجد تحديد ما إذا كانت الرسالة ستُطبع أم لا في التابع Log، كما عرّفنا أيضًا تابعًا غير مُصدّر يدعى write وهو مَن سيطبع الرسالة في نهاية المطاف. يمكننا الآن استخدام مستويات التسجيل في الحزم الأخرى من خلال تعديل ملف cmd/main.go ليصبح كما يلي: package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") } بتشغيل الملف ستحصل على الخرج التالي: [info] 2019-09-23T20:53:38Z starting up service [warning] 2019-09-23T20:53:38Z no tasks found [error] 2019-09-23T20:53:38Z exiting: no work performed نلاحظ نجاح استخدام التابع Log، كما يمكننا تمرير الوسيط level من خلال ضبط القيمة false إلى المتغير debug: package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, false) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") } سنلاحظ أنه سيطبع فقط رسائل مستوى الخطأ (لم يُنفّذ أيّ عمل): [error] 2019-08-28T13:58:52-05:00 exiting: no work performed إذا حاولت استدعاء التابع write من خارج الحزمة logging، فستحصل على خطأ في وقت التصريف: package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") logger.write("error", "log this message...") } يكون الخرج كما يلي: cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write) بمجرد أن بلاحظ المُصرّف أنك تحاول الوصول إلى شيء ما أول محرف منه صغير، فإنه يعلم أنه غير مصدّر وسيرمي خطأ تصريف. يوضّح المسجل في هذا المقال كيف يمكننا كتابة التعليمات البرمجية التي تعرض فقط الأجزاء التي نريد أن تستخدِمها الحزم الأخرى، وبما أننا نتحكم في أجزاء الحزمة التي تظهر خارج الحزمة، فيمكننا لاحقًا إجراء تغييرات دون التأثير على أيّ شيفرة تعتمد على الحزمة الخاصة بنا، فإذا أردنا مثلًا إيقاف تشغيل رسائل مستوى المعلومات فقط عندما يكون debug مضبوطًا على false، فيمكنك إجراء هذا التغيير دون التأثير على أي جزء آخر من واجهة برمجة التطبيقات API الخاصة بك، كما يمكننا أيضًا إجراء تغييرات بأمان على رسالة السجل لتضمين المزيد من المعلومات مثل المجلد الذي كان البرنامج يعمل منه. الخاتمة وضح هذا المقال كيفية مشاركة الشيفرة بين الحزم المختلفة مع حماية تفاصيل تنفيذ الحزمة الخاصة بك، إذ يتيح لك ذلك تصدير واجهة برمجة تطبيقات بسيطة نادرًا ما تحصل عليها تغييرات للتوافق مع الإصدارات السابقة، بحيث تكون التغييرات الأساسية غالبيتها ضمن ذلك الصندوق الأسود، ويُعَدّ هذا من أفضل الممارسات عند إنشاء الحزم وواجهات برمجة التطبيقات المقابلة لها. ترجمة -وبتصرُّف- للمقال Understanding Package Visibility inGo لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: إنشاء الحزم في لغة جو Go كتابة برنامجك الأول في لغة جو Go استيراد الحزم في لغة جو Go
  22. تُعَدّ الحزمة مجموعةً من الملفات الموجودة ضمن مجلد واحد والمتضمّنة لتعليمة الحزمة نفسها في بدايتها، ويمكنك تضمين العديد من الحزم ضمن برنامجك عند الحاجة لبناء برمجيات أكثر تعقيدًا. تكون بعض الحزم موجودةً في مكتبة جو القياسية وبعضها الآخر يمكنك تثبيته من خلال الأمر go get، كما يمكنك أيضًا إنشاء الحزم الخاصة بك من خلال بناء ملفات لغة جو التي تحتاجها ووضعه ضمن المجلد نفسه مع الالتزام بكتابة تعليمة الحزمة الضرورية في بداية كل ملف، كما ستتعلم في هذا المقال كيفية إنشاء حزم لغة جو الخاصة بك لاستخدامها في برامجك. المتطلبات أن يكون لديك مساحة عمل خاصة في لغة جو، فإذا لم يكن لديك، فاتبع سلسلة المقالات التالية، فقد تحدّثنا عن ذلك في بداية السلسلة تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu تثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS تثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز يُفضّل أن تكون قد اطلعت أيضًا على المقالة السابقة يُفضّل أن يكون لديك معرفة حول متغير البيئة أو مسار البيئة GOPATH، ويمكنك الاطلاع على مقال التعرّف على GOPATH كتابة واستيراد الحزم تشبه كتابة الحزم كتابة أيّ برنامج آخر في لغة جو، ويمكن أن تتضمن الحزم دوالًا أو متغيرات أو أنواع بيانات خاصة يمكنك استخدامها في برنامجك لاحقًا. يجب أن تكون ضمن مساحة العمل الخاصة بك قبل إنشاء حزم جديدة، وهذا يعني أن تكون ضمن مسار البيئة gopath، ففي هذا المقال مثلًا سنُنشئ الحزمة greet، لذا سنُنشئ المجلد greet ضمن مسار البيئة وضمن مساحة العمل الخاصة بك، فإذا كنا ضمن مساحة العمل gopherguides وأردنا إنشاء الحزمة greet ضمنها أثناء استخدام جيت هاب Github على أساس مستودع تعليمات برمجية، فسيبدو المجلد كما يلي: └── $GOPATH └── src └── github.com └── gopherguides ستكون الحزمة greet ضمن المجلد gopherguides: └── $GOPATH └── src └── github.com └── gopherguides └── greet يمكننا الآن إنشاء أول ملف في الحزمة، ويسمى الملف الأساسي -ويسمى أيضًا نقطة الدخول entry point- في الحزمة عادةً باسم الحزمة نفسها، إذًا سنُنشئ الملف greet.go ضمن المجلد greet كما يلي: └── $GOPATH └── src └── github.com └── gopherguides └── greet └── greet.go يمكننا بعد إنشاء الملف كتابة التعليمات البرمجية التي نريدها ضمنه، ويكون الهدف من هذه التعليمات عادةً هو الاستخدام في مكان آخر من المشروع، وفي هذه الحالة سننشئ دالة Hello تطبع Hello World، لذا افتح الملف greet.go من خلال محرر الشيفرات الخاص بك واكتب ضمنه التعليمات التالية: package greet import "fmt" func Hello() { fmt.Println("Hello, World!") } يجب دومًا أن نبدأ باسم الحزمة التي نعمل ضمنها لكي نُخبر المصرِّف أنّ هذا الملف هو جزء من الحزمة، لذا كتبنا الكلمة المفتاحية package متبوعةً باسم الحزمة: package greet ثم نكتب اسم الحزم التي نحتاج إلى استخدامها ضمن الملف من خلال وضع أسمائها بعد الكلمة المفتاحية import، وهنا نحتاج إلى حزمة fmt فقط: import "fmt" أخيرًا سنكتب الدالة Hello التي تستخدِم الحزمة fmt لتنسيق طباعة جملة الخرج: func Hello() { fmt.Println("Hello, World!") } نكون الآن قد انتهينا من إنشاء الملف الأول وأصبح بإمكاننا استخدام الدالة Hello منه وفي المكان الذي نريده. سنُنشئ الآن حزمةً جديدةً سنسميها example، لذا يجب أن نُنشئ مجلدًا لها بالاسم نفسه، وسننشئه ضمن مساحة العمل نفسها gopherguides: └── $GOPATH └── src └── github.com └── gopherguides └── example الآن وقد أصبح لديك مجلدًا خاصًا بالحزمة، بات بإمكانك إنشاء ملف نقطة الدخول، والذي سيكون ملفًا رئيسيًا -أي ملف تنفيذي-، لذا سنسميه main.go: └── $GOPATH └── src └── github.com └── gopherguides └── example └── main.go افتح الملف من محرر الشيفرات الخاص بك واكتب التعليمات التالية: package main import "github.com/gopherguides/greet" func main() { greet.Hello() } بما أن الدالة التي تريد استخدامها ضمن الملف الرئيسي موجودة ضمن حزمة أخرى، فسيتوجب عليك استدعاؤها من خلال ذكر اسم الحزمة أولًا متبوعًا بنقطة ثم اسم الدالة، فمثلًا وضعنا هنا اسم الحزمة greet ثم نقطة ثم اسم الدالة ()greet.Hello، ويمكنك الآن فتح الطرفية وتشغيل البرنامج ضمنها: go run main.go سيكون الخرج كما يلي: Hello, World! سنضيف بعض المتغيرات إلى ملف greet.go لتتعلم كيفية استخدام المتغيرات ضمن الحزمة: package greet import "fmt" var Shark = "Sammy" func Hello() { fmt.Println("Hello, World!") } ثم افتح الملف main.go وأضف التعليمة التالية (fmt.Println(greet.Shark لاستدعاء المتغير Shark داخل الدالة fmt.Println، أي كما يلي: package main import ( "fmt" "github.com/gopherguides/greet" ) func main() { greet.Hello() fmt.Println(greet.Shark) } ثم شغّل الشيفرة مرةً أخرى: $ go run main.go ستحصل على الخرج التالي: Hello, World! Sammy أخيرًا، سننشئ نوع بيانات جديد ضمن ملف greet.go، إذ سننشئ نوع البيانات Octopus الذي يتضمّن الحقلين name و color، كما سنعرّف دالةً تطبع هذه الحقول: package greet import "fmt" var Shark = "Sammy" type Octopus struct { Name string Color string } func (o Octopus) String() string { return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color) } func Hello() { fmt.Println("Hello, World!") } سننشئ الآن نسخةً من هذا النوع داخل الملف main.go: package main import ( "fmt" "github.com/gopherguides/greet" ) func main() { greet.Hello() fmt.Println(greet.Shark) oct := greet.Octopus{ Name: "Jesse", Color: "orange", } fmt.Println(oct.String()) } بمجرّد إنشاء نسخة من النوع Octopus عند كتابة oct := greet.Octopus يُصبح بإمكاننا الوصول إلى الداول والمتغيرات الموجودة ضمنه من الملف main، وبالتالي إمكانية استدعاء الدالة ()oct.String من دون الحاجة لكتابة اسم الحزمة greet، كما يمكنك الوصول إلى الحقول بالطريقة نفسها دون الحاجة إلى كتابة اسم الحزمة مثل oct.Color. يستخدِم التابع String التي يتضمنها النوع Octopus الدالة fmt.Sprintf لإنشاء وإرجاع سلسلة في المكان الذي استُدعي فيه أي في هذه الحالة في الملف main، وسنشغّل البرنامج الآن كما يلي: $ go run main.go يكون الخرج كما يلي: Hello, World! Sammy The octopus's name is "Jesse" and is the color orange. إذًا سيصبح لدينا دالة يمكن استخدامها حيثما نريد لطباعة معلومات عن نوع البيانات الذي عرّفناه من خلال تعريفنا للتابع String ضمن النوع Octopus، فإذا أردت تغيير سلوك هذا التابع لاحقًا، فيمكنك ببساطة تعديله فقط حيثما يكون. تصدير الشيفرة لاحظ أنّ كل التصريحات داخل الملف greet.go تبدأ بمحرف كبير، ولا تمتلك لغة جو مفاهيم مُحددات الوصول العامة public والخاصة private والمحمية protected كما في باقي اللغات، ويمكن التحكم بالرؤية في لغة جو من خلال الكتابة بمحارف كبيرة، فالمتغيرات أو الدوال أو الأنواع التي تبدأ بمحارف كبيرة تكون مرئيةً من خارج الحزمة -أي عامة- ويُعتبر عندها مُصدّرًا exported. إذا أضفت تابعًا جديدًا إلى النوع Octopus اسمه reset، فستتمكّن من استدعائه من داخل الحزمة greet، لكن لن تتمكن من استدعائه من الملف main.go لأنه خارج الحزمة greet: package greet import "fmt" var Shark = "Sammy" type Octopus struct { Name string Color string } func (o Octopus) String() string { return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color) } func (o *Octopus) reset() { o.Name = "" o.Color = "" } func Hello() { fmt.Println("Hello, World!") } إذا حاولت استدعاء reset من الملف main.go: package main import ( "fmt" "github.com/gopherguides/greet" ) func main() { greet.Hello() fmt.Println(greet.Shark) oct := greet.Octopus{ Name: "Jesse", Color: "orange", } fmt.Println(oct.String()) oct.reset() } ستحصل على الخطأ التالي والذي يقول أنه لا يمكن الإشارة إلى حقل أو دالة غير مصدرة: oct.reset undefined (cannot refer to unexported field or method greet.Octopus.reset) لتصدير دالة reset من Octopus، اجعل المحرف الأول من الدالة كبيرًا، أي Reset: package greet import "fmt" var Shark = "Sammy" type Octopus struct { Name string Color string } func (o Octopus) String() string { return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color) } func (o *Octopus) Reset() { o.Name = "" o.Color = "" } func Hello() { fmt.Println("Hello, World!") } وبالتالي سيصبح بإمكانك استدعائها من الحزمة الأخرى بدون مشاكل: package main import ( "fmt" "github.com/gopherguides/greet" ) func main() { greet.Hello() fmt.Println(greet.Shark) oct := greet.Octopus{ Name: "Jesse", Color: "orange", } fmt.Println(oct.String()) oct.Reset() fmt.Println(oct.String()) } الآن إذا شغّلت البرنامج: $ go run main.go ستتلقى الخرج التالي: Hello, World! Sammy The octopus's name is "Jesse" and is the color orange The octopus's name is "" and is the color . نلاحظ من خلال استدعاء الدالة Reset أن جميع بيانات الحقول Name و Color قد مسحتها، وعند استدعاء التابع String لاتطبع شيئًا لأن الحقول السابقة قد أصبحت فارغةً. الخاتمة تشبه كتابة الحزم في لغة جو كتابة أيّ ملف في لغة جو، إلا أن وضعها في مجلد مُختلف سيسمح لك بعزل الشيفرة لإعادة استخدامها في مكان آخر، وقد تحدثنا في هذا المقال عن كيفية تعريف وإنشاء الحزم وكيفية الاستفادة منها في ملفات أخرى وأين يمكن وضعها للوصول إليها. ترجمة -وبتصرُّف- للمقال How To Write Packages in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: استيراد الحزم في لغة جو Go التعرف على الروابط Maps في لغة جو Go تحويل أنواع البيانات في لغة جو Go التعامل مع السلاسل في لغة جو Go استخدام المتغيرات والثوابت في لغة جو Go
  23. عادةً ما نحتاج إلى استخدام دوال خارجية أخرى في البرنامج، هذه الدوال تكون ضمن حزم خارجية بناها آخرون أو بنيناها بأنفسنا لتقسيم البرنامج إلى عدة ملفات بهدف جعل البرنامج أبسط وأقل تعقيدًا وأكثر أناقةً وقابليةً للفهم. تُمثّل الحزمة مجلدًا يحتوي على ملف أو عدة ملفات، هذه الملفات هي شيفرات جو قد تتضمن دوال أو أنواع بيانات أو واجهات يمكنك استخدامها في برنامجك الأساسي، وسنتحدث في هذا المقال عن تثبيت الحزم واستيرادها وتسميتها. حزمة المكتبة القياسية تُعَدّ المكتبة القياسية التي تأتي مع جو مجموعةً من الحزم، إذ تحتوي هذه الحزم على العديد من اللبنات الأساسية لكتابة البرامج المتطورة في لغة جو، فتحتوي الحزمة fmt مثلًا على العديد من الدوال التي تُسهّل عمليات تنسيق وطباعة السلاسل النصية، كما تحتوي حزمة net/http على دوال تسمح للمطوّر بإنشاء خدمات ويب وإرسال واسترداد البيانات عبر بروتوكول http إضافةً للعديد من الأمور الأخرى. يجب عليك أولًا استيراد هذه الحزمة باستخدام التعليمة import متبوعة باسم الحزمة للاستفادة من الدوال الموجودة ضمن أيّ حزمة، فيمكنك مثلًا استيراد الحزمة math/rand ضمن الملف random.go لتوليد أعداد عشوائية: import "math/rand" عند استيراد حزمة ما داخل برنامج تصبح جميع مكوناتها قابلةً للاستخدام داخل هذا البرنامج لكن بفضاء أسماء منفصل namespace، وهذا يعني أنه يجب الوصول إلى كل دالة من الحزمة الخارجية بالاعتماد على تدوين النقطة dot notation، أي package.function، فللوصول مثلًا إلى الدوال الموجودة في الحزمة السابقة نكتب ()rand.Int لتوليد عدد صحيح عشوائي أو ()rand.Intn لتوليد عدد عشوائي بين الصفر والعدد المحدَّد. سنستخدِم في المثال التالي حلقة for لتوليد أعداد عشوائية بالاستعانة بالدوال السابقة: package main import "math/rand" func main() { for i := 0; i < 10; i++ { println(rand.Intn(25)) } } يكون ناتج الخرج كما يلي: 6 12 22 9 6 18 0 15 6 0 نستورد بدايةً الحزمة math/rand ثم نستخدِم حلقة تتكرر 10 مرات وفي كل مرة تطبع عدد صحيح بين 0 و 25 بحيث تكون الأعداد الناتجة أقل تمامًا من 25 بما أننا مررنا العدد 25 للدالة ()rand.Intn، وطبعًا عملية التوليد هي عشوائية، لذا ستحصل على أعداد مختلفة في كل مرة تُنفِّذ فيها هذا المقطع البرمجي. عند استيراد أكثر من حزمة نضع هذه الحزم بين قوسين () بعد الكلمة import بدلًا من استخدام الكلمة import من أجل كل حزمة، إذ سيجعل هذا الشيفرة مرتبةً أكثر كما في المثال التالي: import ( "fmt" "math/rand" ) سنستخدِم الحزمة الجديدة التي استوردناها من أجل تنسيق عملية الطباعة في الشيفرة السابقة: package main import ( "fmt" "math/rand" ) func main() { for i := 0; i < 10; i++ { fmt.Printf("%d) %d\n", i, rand.Intn(25)) } } يكون الخرج كما يلي: 0) 6 1) 12 2) 22 3) 9 4) 6 5) 18 6) 0 7) 15 8) 6 9) 0 تعلّمت في هذا القسم كيفية استيراد الحزم واستخدامها لكتابة برنامج أكثر تعقيدًا، لكن لم نستخدِم إلا الحزم الموجودة في المكتبة القياسية، وفيما يلي سنرى كيفية تثبيت واستخدام الحزم التي كتبها مطورون آخرون. تثبيت الحزم تحتوي المكتبة القياسية على العديد من الحزم الهامة والمفيدة لكنها لأغراض عامة، لذا يُنشئ المطورون حزمًا خاصةً بهم فوق المكتبة القياسية لتلبية احتياجاتهم الخاصة. تتضمن أدوات جو الأمر go get الذي يسمح بتثبيت حزم خارجية ضمن بيئة التطوير المحلية الخاصة بك واستخدامها في برنامجك، وعند استخدام هذا الأمر تُشاع الإشارة إلى الحزمة من خلال مسارها الأساسي، كما يمكن أن يكون هذا المسار مسارًا إلى مشروع عام مُستضاف في مستودع شيفرات مثل جيت هاب GitHub، فلاستيراد الحزمة flect مثلًا نكتب: $ go get github.com/gobuffalo/flect سيعثر الأمر go get على هذه الحزمة الموجودة على جيت هاب وسيثبتها في GOPATH$، إذ ستُثبّت الحزمة في هذا المثال ضمن المجلد: $GOPATH/src/github.com/gobuffalo/flect غالبًا ما تُحدَّث الحزم من قِبل مُطوريها لمعالجة الزلات البرمجية أو إضافة ميزات جديدة، وقد ترغب في هذه الحالة باستخدام أحدث إصدار من تلك الحزمة للاستفادة من الميزات الجديدة أو الزلة البرمجية التي عالجوها، إذ يمكنك تحديث حزمة ما باستخدام الراية ‎-u مع الأمر go get: $ go get -u github.com/gobuffalo/flect سيؤدي هذا الأمر أيضًا إلى تثبيت الحزمة إذا لم تكن موجودةً لديك أساسًا، ويؤدي الأمر go get دائمًا إلى تثبيت أحدث إصدار من الحزمة المتاحة، لكن قد ترغب في استخدام حزمة سابقة أو قد تكون هناك نسخة سابقة قد حُدّثت وترغب في استخدامها، ففي هذه الحالة ستحتاج إلى استخدام أداة إدارة الحزم مثل Go Modules، واعتبارًا من الإصدار Go 1.11 اعتُمِدت Go Modules بوصفها أداة إدارة حزم رسمية لجو. تسمية الحزم بأسماء بديلة قد تحتاج إلى تغيير اسم الحزمة التي تستوردها في حال كان لديك حزمة أخرى تحمل الاسم نفسه، ففي هذه الحالة يمكنك استخدام الكلمة alias لتغيير اسم الحزمة وتجنب تصادم الأسماء وفق الصيغة التالية: import another_name "package" سنعدِّل في المثال التالي اسم الحزمة fmt ضمن ملف random.go ليصبح f: package main import ( f "fmt" "math/rand" ) func main() { for i := 0; i < 10; i++ { f.Printf("%d) %d\n", i, rand.Intn(25)) } } كتبنا f.Printf في البرنامج السابق بدلًا من fmt.Printf، وعمومًا لا يُفضَّل في جو استخدام الأسماء البديلة كما في باقي اللغات مثل بايثون لأنه خروج عن النمط الشائع. عند اللجوء إلى الأسماء البديلة لتجنب حدوث تصادم في الأسماء، يفضَّل إعادة تسمية الحزم المحلية وليس الخارجيّة، فإذا كان لديك مثلًا حزمة أنشأتها اسمها strings وتريد استخدامها في برنامجك إلى جانب حزمة النظام strings، فيُفضّل تسمية الحزمة التي أنشأتها أنت وليس حزمة النظام. تذكَّر دومًا أنّ سهولة القراءة والوضوح في برنامجك أمر مهم، لذا يجب عليك استخدام الأسماء البديلة فقط لجعل الشيفرة أكثر قابليةً للقراءة أو عندما تحتاج إلى تجنب تضارب الأسماء. تنسيق الحزم يمكنك فرز الحزم بترتيب معيّن من خلال تنسيق عمليات الاستيراد لجعل شفرتك أكثر اتساقًا ومنع حدوث تغييرات أو إيداعات عشوائية random commits عندما يكون الشيء الوحيد الذي يتغير هو ترتيب فرز عمليات الاستيراد، فمنع الإيداعات العشوائية سيمنع حدوث خلل في التعليمات البرمجية أو ارتباك أثناء مراجعة الشفرة من قِبل الآخرين. تُنسّق معظم محررات الشيفرات استيراد الحزم تلقائيًا أو يسمحون لك باستخدام الأداة goimports، إذ يُعَدّ استخدام هذه الأداة أمرًا مفيدًا وشائعًا وممارسةً قياسيةً، فالحفاظ على ترتيب الفرز يدويًا قد يكون مُملًا ومعرّضًا للأخطاء، بالإضافة إلى ذلك، إذا أُجريَت أية تغييرات على التنسيق القياسي لفرز الحزم، فستُحدَّث goimports لتعكس تلك التغييرات في التنسيق، مما يضمن لك ولشركائك أنه سيكون لديكم تنسيق متسق لكتل الاستيراد، وإليك ما قد تبدو عليه كتلة الاستيراد قبل التنسيق: import ( "fmt" "os" "github.com/digital/ocean/godo" "github.com/sammy/foo" "math/rand" "github.com/sammy/bar" ) سيكون لديك الآن التنسيق التالي بتشغيل الأداة goimport -أو في حالة مُحرّر يستخدِم هذه الأداة، فسيؤدي حفظ الملف إلى تشغيله نيابة عنك-: import ( "fmt" "math/rand" "os" "github.com/sammy/foo" "github.com/sammy/bar" "github.com/digital/ocean/godo" ) لاحظ أنه يُجمِّع حزم المكتبة القياسية أولًا ثم الحزم الخارجية مع فصلها بسطور فارغة مما يجعل من السهل قراءة وفهم الحزم المستخدَمة. سيؤدي استخدام الأداة goimports إلى الحفاظ على تنسيق جميع كتل الاستيراد بطريقة مناسبة، ويمنع حدوث ارتباك حول التعليمات البرمجية بين المطورين الذين يعملون على الملفات نفسها. الخاتمة تحدّثنا في هذا المقال عن كيفية استيراد حزم المكتبة القياسية وكيفية استيراد وتثبيت الحزم الخارجية باستخدام go get للاستفادة من محتوياتها، كما تحدّثنا أيضًا عن كيفية تحديث الحزم واستخدام الأسماء البديلة معها، إذ يتيح لنا استخدام الحزم جعل برامجنا أكثر قوةً ومتانةً وقابليةً للصيانة وللاستخدام المتعدِّد من خلال تقسيمه إلى أجزاء مستقلّة. ترجمة -وبتصرُّف- للمقال Importing Packages in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: معالجة حالات الانهيار في لغة جو Go فهم، تنصيب وتهيئة بيئة عمل لغة البرمجة Go التعامل مع السلاسل في لغة جو Go التعرف على الروابط Maps في لغة جو Go
  24. تنقسم الأخطاء التي قد تحدث في البرنامج إلى فئتين رئيسيتين هما أخطاء يتوقع المبرمج حدوثها وأخطاء لم يتوقع حدوثها، وتُعالِج الواجهة error التي تحدّثنا عنها في المقال السابق إلى حد كبير الأخطاء التي نتوقعها أثناء كتابة البرامج حتى تلك الأخطاء التي تكون احتمالات حدوثها نادرةً. تندرج حالات الانهيار Panics تحت الفئة الثانية والتي تؤدي إلى إنهاء البرنامج تلقائيًا والخروج منه، وعادةً تكون الأخطاء الشائعة هي سبب حالات الانهيار، لذا سندرس في هذا المقال بعض الحالات الشائعة التي يمكن أن تؤدي إلى حالات الانهيار، كما سنلقي نظرةً أيضًا على بعض الطرق التي يُمكن أن نتجنب من خلالها حالات الانهيار، وسنستخدم أيضًا تعليمات التأجيل defer statements جنبًا إلى جنب مع الدالة recover لالتقاط حالات الانهيار قبل أن تؤدي إلى إيقاف البرنامج. ما هي حالات الانهيار؟ هناك عمليات محددة في لغة جو تؤدي إلى حالات الانهيار ومن ثم إيقاف البرنامج مباشرةً، وتتمثّل بعض هذه العمليات في محاولة الوصول إلى فهرس لا تتضمنه حدود المصفوفة -أي تجاوز حدود المصفوفة- أو إجراء عمليات توكيد النوع type assertions أو استدعاء توابع على مؤشرات لا تُشير إلى أيّ شيء nil أو استخدام كائنات المزامنة mutexes بطريقة خاطئة أو محاولة العمل مع القنوات المغلقة closed channels، فمعظم هذه المواقف تنتج عن أخطاء أثناء البرمجة ولا يستطيع المُصرِّف اكتشافها أثناء تصريف البرنامج. بما أن حالات الانهيار تتضمن تفاصيل مفيدةً لحل مشكلة ما، فعادةً ما يستخدِم المطورون حالات الانهيار بوصفها مؤشرًا على ارتكابهم خطأ أثناء تطوير البرنامج. حالات الانهيار الناتجة عن تجاوز الحدود ستولد جو حالة انهيار في وقت التشغيل runtime عند محاولة الوصول إلى فهرس خارج حدود المصفوفة أو الشريحة، ويجسِّد المثال التالي هكذا حالة، إذ سنحاول الوصول إلى آخر عنصر من الشريحة من خلال القيمة المُعادة من الدالة len، أي من خلال طول الشريحة. package main import ( "fmt" ) func main() { names := []string{ "lobster", "sea urchin", "sea cucumber", } fmt.Println("My favorite sea creature is:", names[len(names)]) } يكون الخرج كما يلي: panic: runtime error: index out of range [3] with length 3 goroutine 1 [running]: main.main() /tmp/sandbox879828148/prog.go:13 +0x20 لاحظ أننا حصلنا على تلميح panic: runtime error: index out of range والذي يشير إلى أننا نحاول الوصول إلى فهرس خارج حدود المصفوفة. أنشأنا شريحة names بثلاث قيم ثم حاولنا طباعة العنصر الأخير من خلال القيمة المُعادة من الدالة ()len والتي ستُعيد القيمة 3، لكن آخر عنصر في المصفوفة يملك الفهرس 2 وليس 3 بما أنّ الفهرسة تبدأ من الصفر وليس من الواحد، لذا في هذه الحالة لا يوجد خيارًا لوقت التشغيل إلا أن يتوقف ويُنهي البرنامج، أيضًا لا يمكن لجو أن تُبرهِن أثناء عملية التصريف أن الشيفرة ستحاول فعل ذلك، لذلك لا يمكن للمُصرِّف التقاط هذا. تُسبب حالة الانهيار إيقاف تنفيذ برنامجك تمامًا، وبالتالي لن تُنفّذ أيّة تعليمات برمجية تالية، كما تحتوي الرسالة الناتجة على العديد من المعلومات المفيدة في تشخيص سبب حالة الانهيار. مكونات حالة الانهيار تتكون حالات الانهيار من رسالة تحاول توضيح سبب حالة الانهيار إضافةً إلى مسار المكدس stack trace الذي يساعدك على تحديد مكان حدوث حالة الانهيار في تعليماتك البرمجية، إذ تبدأ رسالة الانهيار بالكلمة panic:‎ تتبعها سلسلة نصية توضيحية تختلف تبعًا لسبب حالة الانهيار، فرسالة الانهيار في المثال السابق كانت كما يلي: panic: runtime error: index out of range [3] with length 3 تخبرنا السلسلة runtime error:‎ التي تتبع الكلمة panic:‎ أنّ الخطأ قد حدث في وقت التشغيل؛ أما بقية الرسالة، فتخبرنا بأن الفهرس 3 خارج حدود الشريحة. الرسالة التالية هي رسالة مسار المكدس، وهي خريطة يمكننا تتبُعها لتحديد سطر التعليمات البرمجية الذي أدى تنفيذه إلى حدوث حالة الانهيار وكيفية استدعاء هذه الشيفرة من شيفرة أخرى. goroutine 1 [running]: main.main() /tmp/sandbox879828148/prog.go:13 +0x20 يوضّح مسار المكدس هذا أن حالة الانهيار قد حدثت في الملف ‎/tmp/sandbox879828148/prog.go في السطر رقم 13، كما يخبرنا أيضًا أنه نشأ في الدالة ()main من الحزمة الرئيسية main. ينقسم مسار المكدس إلى عدة أقسام بحيث يكون هناك قسم لكل روتين goroutine فكل عملية تنفيذ لبرنامج في جو تُنجز من خلال روتين واحد أو أكثر، أي كل روتين يُنفذ جزء محدد من الشيفرة، وكل من هذه الروتينات يُمكن تنفيذها باستقلالية عن باقي الروتينات وبالتزامن معها. تبدأ كل كتلة بالرأس :[goroutine X [state، تُمثّل الإشارة X إلى رقم الروتين ID (مُعرّفه) مع الحالة التي كان عليها عندما حدثت حالة الانهيار، ويعرض مسار المكدس بعد الرأس الدالة التي حدثت فيها حالة الانهيار أثناء تنفيذها إلى جانب اسم الملف ورقم سطر تنفيذ الدالة. الإشارة إلى العدم يمكن أن تحدث حالة الانهيار أيضًا عند محاولة استدعاء تابع على مؤشر لا يُشير إلى أيّ شيء، إذ تُمكننا المؤشرات في جو من الإشارة إلى نسخ مُحددة من بعض الأنواع الموجودة في الذاكرة خلال وقت التشغيل، وعندما لا يُشير المؤشر إلى أيّ شيء، فستكون قيمة المؤشر nil، وعندما نحاول استدعاء توابع على مؤشر لا يشير إلى شيء، فستظهر حالة انهيار، والأمر ذاته بالنسبة للمتغيرات التي تكون من أنواع الواجهات، إذ تولّد أيضًا حالة الانهيار إذا استُدعيت توابع عليها كما في المثال التالي: package main import ( "fmt" ) type Shark struct { Name string } func (s *Shark) SayHello() { fmt.Println("Hi! My name is", s.Name) } func main() { s := &Shark{"Sammy"} s = nil s.SayHello() } يكون الخرج كما يلي: panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba] goroutine 1 [running]: main.(*Shark).SayHello(...) /tmp/sandbox160713813/prog.go:12 main.main() /tmp/sandbox160713813/prog.go:18 +0x1a عرّفنا في هذا المثال سجل struct اسميناه Shark يحتوي على تابع وحيد مُعرَّف ضمن مُستقبل مؤشرها واسمه SayHello والذي يطبع تحية عند استدعائه، كما نُنشئ داخل جسم الدالة main نسخةً جديدةً من السجل Shark ونحاول أخذ مؤشر عليها باستخدام العامِل &، إذ يُسنَد المؤشر إلى المتغير s ثم نُسند المتغير s مرةً أخرى إلى القيمة nil من خلال التعليمة s = nil ثم نحاول استدعاء التابع SayHello على المتغيرs. نتيجةً لذلك نحصل على حالة انهيار تُشير إلى محاولة الوصول إلى عنوان ذاكرة غير صالح لأن المتغير s كانت قيمته nil، وبالتالي عند استدعاء الدالة SayHello فإنه يحاول الوصول إلى الحقل Name الموجود على النوع ‎*Shark، وبالتالي تحدث حالة انهيار لأنه لا يمكن تحصيل dereference قيمة غير موجودة nil بما أنّ المؤشر هو مؤشر استقبال والمستقبل في هذه الحالة هو nil. حددنا في هذا المثال القيمة nil صراحةً، لكن تظهر هذه القيم في الحالات العملية من دون انتباهنا، إذًا، عندما تظهر لديك حالات انهيار تتضمن الرسالة nil pointer dereference، ينبغي عليك التحقق مما إذا كنت تُسند قيمة nil إلى أحد المؤشرات التي تستخدِمها في برنامجك. استخدام دالة panic المضمنة حالات الانهيار الناتجة عن تجاوز حدود الشريحة أو تلك الناتجة عن المؤشرات ذات القيمة nil هي الأشهر، لكن من الممكن أيضًا أن نُظهر حالة انهيار يدويًا من خلال الدالة المُضمّنة panic التي تأخذ وسيط واحد فقط يُمثِّل السلسلة النصية التي ستمثّل رسالة الانهيار، وعادةً ما يكون عرض هذه الرسالة أكثر راحةً من تعديل الشيفرة بحيث تعيد خطأً، كما يمكننا أيضًا استخدامها داخل الحزم الخاصة بنا لتنبيه المطورين إلى أنهم قد ارتكبوا خطأً عند استخدام شيفرة الحزمة خاصتنا، عمومًا، يُفضَّل إعادة قيم الخطأ error إلى مُستخدمِي الحزمة، وسيطلق المثال التالي حالة انهيار ناتجةً عن دالة تُستدعى من دالة أخرى: package main func main() { foo() } func foo() { panic("oh no!") } ستكون حالة الانهيار الناتجة كما يلي: panic: oh no! goroutine 1 [running]: main.foo(...) /tmp/sandbox494710869/prog.go:8 main.main() /tmp/sandbox494710869/prog.go:4 +0x40 عرّفنا في هذا المثال الدالة foo التي تستدعي الدالة panic والتي نُمرر لها السلسلة "oh no!‎" ثم استدعينا هذه الدالة foo من داخل الدالة main. لاحظ أنّ الخرج يتضمن الرسالة التي حددناها للدالة panic ويعرض مسار المكدس لنا روتينًا واحدًا وسطرين من المسارات؛ الأول للدالة ()main والثاني للدالة ()foo. وجدنا مما سبق أنّ حالات الانهيار تؤدي إلى إنهاء البرنامج لحظة حدوثها، ويمكن أن يؤدي ذلك إلى حدوث مشكلات عند وجود موارد مفتوحة يجب إغلاقها بطريقة سليمة، وهنا توفِّر جو آليةً لتنفيذ بعض التعليمات البرمجية دائمًا حتى عندما تظهر حالة الانهيار. الدوال المؤجلة قد يحتوي برنامجك على موارد يجب تنظيفها بطريقة سليمة، حتى أثناء معالجة حالات الانهيار في وقت التشغيل، إذ تسمح لك جو بتأجيل تنفيذ استدعاء دالة ريثما تكون الدالة المُستدعاة ضمنها قد أكتمل تنفيذها. يُمكن للدوال المؤجلة أن تُنفّذ حتى عند ظهور حالة انهيار وتستخدم بوصفها تقنية أمان للحماية من الفوضى والمشكلات التي قد تسببها حالة الانهيار، ولتأجيل دالة نستخدِم الكلمة المفتاحية defer قبل اسم الدالة كما يلي ()defer sayHello، ولاحظ في المثال التالي أن الرسالة ستظهر رغم حدوث حالة انهيار: package main import "fmt" func main() { defer func() { fmt.Println("hello from the deferred function!") }() panic("oh no!") } يكون الخرج كما يلي: hello from the deferred function! panic: oh no! goroutine 1 [running]: main.main() /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55 نؤجل استدعاء دالة مجهولة الاسم داخل الدالة ()main والتي تطبع الرسالة "hello from the deferred function!‎" ثم تُطلق الدالة ()main حالة انهيار من خلال استدعاء الدالة panic، ونلاحظ من الخرج أنّ الدالة المؤجلة تُنفّذ أولًا وتُطبع الرسالة الخاصة بها، ثم تظهر لنا رسالة حالة الانهيار. توفر جو لنا أيضًا فرصة منع حالة الانهيار من إنهاء البرنامج من داخل الدوال المؤجلة من خلال دالة مُضمّنة أخرى. معالجة حالات الانهيار تمتلك جو تقنية معالجة واحدة تتجسّد في الدالة recover، إذ تسمح لك هذه الدالة في اعتراض حالة الانهيار من مسار المكدس ومنع الإنهاء المُفاجئ للبرنامج. هناك قواعد صارمة عند التعامل معها، لكنها دالة لا تقُدّر بثمن في تطبيقات الإنتاج، ويمكن استدعاء الدالة recover مباشرةً دون الحاجة إلى استيراد أيّ حزمة لأنها دالة مُضمّنة كما ذكرنا. package main import ( "fmt" "log" ) func main() { divideByZero() fmt.Println("we survived dividing by zero!") } func divideByZero() { defer func() { if err := recover(); err != nil { log.Println("panic occurred:", err) } }() fmt.Println(divide(1, 0)) } func divide(a, b int) int { return a / b } يكون الخرج كما يلي: 2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero we survived dividing by zero! تُستدعى الدالة divideByZero من داخل الدالة main، كما داخل هذه الدالة نؤجل استدعاء دالة مجهولة الاسم ومسؤولة عن التعامل مع أيّ حالات انهيار قد تنشأ أثناء تنفيذ divideByZero، ونستدعي داخل هذه الدالة مجهولة الاسم الدالة المُضمّنة recover ونسند الخطأ الذي تُعيده إلى المتغير err. إذا ظهرت حالة انهيار في الدالة ()divideByZero، فستُهيّأ قيمة هذا الخطأ، وإلا فستكون nil، ومن خلال مقارنة قيمة المتغير err بالقيمة nil، سنستطيع تحديد فيما إذا كانت حالة الانهيار قد حدثت، وفي هذه الحالة نسجّل حالة الانهيار من خلال الدالة log.Println كما لو كانت أيّ خطأ آخر. نلاحظ أننا نستدعي الدالة divide ونحاول طباعة ناتجها باستخدام الدالة ()fmt.Println بتتبع الدالة المؤجلة مجهولة الاسم بما أن وسيط هذه الدالة سيُسبب القسمة على صفر، وبالتالي ستظهر حالة انهيار. يُظهر خرج البرنامج رسالة الدالة ()log.println من الدالة مجهولة الاسم والتي تُعالج حالة الانهيار متبوعة برسالة we survived dividing by zero!‎، والتي تُشير إلى أننا منعنا حالة الانهيار من إيقاف البرنامج، وذلك بفضل استخدام دالة المعالجة recover. قيمة الخطأ err المُعادة من الدالة ()recover هي نفسها القيمة المُعطاة لاستدعاء الدالة ()panic، لذا من المهم التأكد مما إذا كانت قيمتها nil. اكتشاف حالات الانهيار باستخدام recover تعتمد الدالة recover على قيمة الخطأ في اكتشاف حدوث حالات الانهيار، وبما أن وسيط الدالة panic هو واجهة فارغة، لذا يمكن أن تكون من أي نوع، كما أنّ القيمة الصفرية لأي نوع بيانات يُمثّل واجهةً هو nil، ويوضِّح المثال التالي أنه يجب تجنب تمرير القيمة nil على أساس وسيط للدالة panic: package main import ( "fmt" "log" ) func main() { divideByZero() fmt.Println("we survived dividing by zero!") } func divideByZero() { defer func() { if err := recover(); err != nil { log.Println("panic occurred:", err) } }() fmt.Println(divide(1, 0)) } func divide(a, b int) int { if b == 0 { panic(nil) } return a / b } يكون الخرج كما يلي: we survived dividing by zero! هذا المثال هو المثال السابق نفسه مُتضمنًا الدالة recover مع بعض التعديلات الطفيفة، إذ عدّلنا دالة القسمة للتحقق مما إذا كان المقسوم عليه b يساوي 0، فإذا كان كذلك، فسيولّد حالة انهيار باستخدام الدالة panic مع وسيط من القيمة nil. لن يتضمن الخرج في هذه المرة رسالة الدالة log التي تعرض حدوث حالة الانهيار بالرغم من حدوث حالة انهيار نتيجة القسمة، ويؤكد هذا السلوك الصامت أهمية التحقق من قيمة الخطأ فيما إذا كانت nil. الخاتمة رأينا العديد من الطرق التي تؤدي إلى ظهور حالات انهيار في البرنامج، كما أننا عرضنا طرقًا لإنشائها يدويًا، ثم راجعنا الدالة المضمنة recover التي تُمكّننا من معالجة حالات الانهيار، وليس من الضروري أن تحتاج إلى التعامل مع حالات الانهيار في برامجك، لكن عندما يتعلق الأمر بتطبيقات الإنتاج فهي بغاية الأهمية. ترجمة -وبتصرُّف- للمقال Handling Panics inGo لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: معالجة الأخطاء في لغة جو Go 3 طرائق لنسخ الملفات في Go التعرف على الروابط Maps في لغة جو Go البيانات المنطقية Boolean في لغة جو Go تحويل أنواع البيانات في لغة جو Go
  25. تتصف البرامج التي تتسم بالمتانة بأنها قادرة على التعامل مع الأخطاء المتوقعة وغير المتوقعة التي قد تحدث عند استخدام البرنامج، فهناك أخطاء ناتجة عن مدخلات غير صحيحة من المستخدِم أو حدوث خطأ في عملية الاتصال بالشبكة، …إلخ. نقصد بمعالجة الأخطاء Error handling التقاط الأخطاء التي تولّدها برامجنا وإخفائها عن المستخدم، فرسائل الأخطاء لا تخيف المبرمجين وإنما يمكن توقعها أحيانًا، إلا أن المستخدِم لا يتوقع رؤيتها وهي تربكه وتسبب له الحيرة، فإن كان سيرى رسالة خطأ للضرورة، فلتكن رسالةً سهلة الفهم، وحتى في هذه الحالة سيرغب المستخدِم في أن يحل المبرمج المشكلة، وهنا يأتي دور التعامل مع الأخطاء ومعالجتها، إذ توفِّر كل لغة تقريبًا آليةً لالتقاط الأخطاء عند حدوثها لمعرفة الأجزاء التي تعطلت واتخاذ الإجراء المناسب لإصلاح المشكلة. لمعالجة الأخطاء في لغات البرمجة الأخرى، عادةً ما يتطلب الأمر من المُبرمجين استخدام بنية قواعد محدَّدة، إلا أنّ الأمر مُختلف في جو، إذ تُعَدّ الأخطاء قيمًا مع نوع الخطأ الذي يُعاد من الدالة مثل أيّ قيمة معادة أخرى، ولمعالجة الأخطاء في لغة جو، يجب عليك فحص هذه الأخطاء التي قد تُعيدها الدوال وتحديد ما إذا كان هناك خطأ فعلًا، واتخاذ الإجراء المناسب لحماية البيانات وإخبار المستخدِمين أو أجزاء البرنامج الأخرى بحدوث الخطأ. إنشاء الأخطاء قبل أن تبدأ بمعالجة الأخطاء عليك إنشاؤها أولًا، إذ توفر المكتبة القياسية دالتين مضمنتين لإنشاء أخطاء وهما ()errors.New و ()fmt.Errorfبحيث تتيح لك هاتان الدالتان تحديد رسالة خطأ مخصصة يمكنك تقديمها لاحقًا للمستخدِمين. تأخذ الدالة ()errors.New وسيطًا واحدًا يمثِّل سلسلةً تُمثّل رسالة الخطأ، بحيث يمكنك تخصيصها لتنبيه المستخدِمين بالخطأ، وسنستخدِم في المثال التالي الدالة ()errors.New لإنشاء خطأ وستكون رسالة الخطأ هي "barnacles" ثم سنطبع هذا الخطأ من خلال الدالة ()fmt.Println، ولاحظ أننا كتبنا جميع أحرف الرسالة بدون استخدام محارف كبيرة تقيّدًا بالطريقة التي تكتب بها الأخطاء في جو والتي تُكتب بمحارف صغيرة. package main import ( "errors" "fmt" ) func main() { err := errors.New("barnacles") fmt.Println("Sammy says:", err) } يكون الخرج كما يلي: Sammy says: barnacles تسمح لك الدالة ()fmt.Errorf بإنشاء رسالة خطأ ديناميكيًا، بحيث يمثِّل الوسيط الأول لهذه الدالة سلسلةً تمثِّل رسالة الخطأ مع إمكانية استخدام العناصر النائبة مثل العنصر s% لينوب عن سلسلة نصية والعنصر d% لينوب عن عدد صحيح؛ أما الوسيط الثاني لهذه الدالة، فهو قيم العناصر النائبة بالترتيب كما في المثال التالي: package main import ( "fmt" "time" ) func main() { err := fmt.Errorf("error occurred at: %v", time.Now()) fmt.Println("An error happened:", err) } يكون الخرج كما يلي: An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103 استخدمنا الدالة ()fmt.Errorf لإنشاء رسالة خطأ تتضمن التوقيت الزمني الحالي، إذ تتضمن السلسلة المُعطاة إلى الدالة ()fmt.Errorf العنصر النائب v% والذي سيأخذ القيمة ()time.Now لاحقًا عند استدعاء هذا الخطأ، وأخيرًا نطبع الخطأ من خلال دالة الطباعة كما فعلنا مع الدالة السابقة. معالجة الأخطاء ما سبق كان مُجرّد مثال لتوضيح كيفية إنشاء الأخطاء؛ أما الآن فستتعلم كيفية إنشائها وتوظيفها، فمن الناحية العملية، يكون من الشائع جدًا إنشاء خطأ وإعادته من دالة عندما يحدث خطأ ما، وبالتالي عند استدعاء هذه الدالة يمكن استخدام عبارة if لمعرفة ما إذا كان الخطأ موجودًا أم أنه لايوجد خطأ، فعندما لا يكون هناك خطأً سنجعل الدالة تعيد قيمة nil. package main import ( "errors" "fmt" ) func boom() error { return errors.New("barnacles") } func main() { err := boom() if err != nil { fmt.Println("An error occurred:", err) return } fmt.Println("Anchors away!") } يكون الخرج كما يلي: An error occurred: barnacles عرّفنا في هذا المثال دالة تُسمى boom()‎ تُعيد خطأً يُنشأ من خلال الدالة errors.New لاحقًا عند استدعاء هذه الدالة والتقاط الخطأ في السطر err := boom، فبعد إسناد الخطأ إلى المتغير err سنتحقق مما إذا كان موجودًا من خلال التعليمة if err != nil، وفي هذا المثال ستكون نتيجة الشرط دومًا true لأننا نُعيد دومًا خطأً من الدالة، وطبعًا لن يكون الأمر دائمًا هكذا، لذلك يجب أن نحدد في الدالة الحالات التي يحدث فيها خطأ والحالات التي لا يحدث فيها خطأ وتكون القيمة المعادة nil. نستخدِم دالة الطباعة ()fmt.Println لطباعة الخطأ كما في كل مرة عند وجود خطأ، ثم نستخدِم أخيرًا تعليمة return لكي لا تُنفّذ تعليمة ("fmt.Println("Anchors away!‎، فهذه التعليمة يجب أن تُنفّذ فقط عند عدم وجود خطأ. ملاحظة: تُعَدّ التعليمة if err != nil محورًا أساسيًا في عملية معالجة الأخطاء، فهي تنقل البرنامج إلى مسار مختلف عن مساره الأساسي في حال وجود خطأ أو تتركه يكمل مساره الطبيعي. تتيح لك جو إمكانية استدعاء الدالة ومعالجة أخطاءها ضمن تعليمة الشرط if مباشرةً، ففي المثال التالي سنكتب المثال السابق نفسه لكن من خلال الاستفادة من هذه السمة كما يلي: package main import ( "errors" "fmt" ) func boom() error { return errors.New("barnacles") } func main() { if err := boom(); err != nil { fmt.Println("An error occurred:", err) return } fmt.Println("Anchors away!") } يكون الخرج كما يلي: An error occurred: barnacles لاحظ أننا لم نغير كثيرًا عن الشيفرة السابقة، فكل ما فعلناه هو أننا استدعينا الدالة التي تُعيد الخطأ واختبرنا الشرط في السطر نفسه ضمن التعليمة if. تعلمنا في هذا القسم كيفية التعامل مع الدوال التي تعيد الخطأ فقط، وهذه الدوال شائعة، لكن من المهم أيضًا أن تكون قادرًا على معالجة الأخطاء من الدوال التي يمكن أن تُعيد قيمًا متعددةً. إعادة الأخطاء والقيم غالبًا ما تُستخدَم الدالات التي تُعيد قيمة خطأ واحدة في الحالات التي نحتاج فيها إلى إحداث تغييرات مُحددة مثل إدراج صفوف في قاعدة بيانات أي عندما نحتاج لمعرفة الحال الذي انتهت عليه العملية، ومن الشائع أيضًا كتابة دالات تُعيد قيمة إذا اكتملت العملية بنجاح أو خطأ مُحتمل إذا فشلت هذه العملية، كما تسمح جو للدالات بإعادة أكثر من نتيجة واحدة، وبالتالي إمكانية إعادة قيمة ونوع الخطأ في الوقت نفسه. لإنشاء دالة تُعيد أكثر من قيمة واحدة، يجب تحديد أنواع كل قيمة مُعادة داخل أقواس ضمن ترويسة الدالة، فالدالة capitalize مثلًا، تُعيد سلسلة string وخطأ error، وقد صرّحنا عن ذلك بكتابة شيفرة كتلية كما يلي: func capitalize(name string) (string, error) {} يخبر الجزء (سلسلة، خطأ) مُصرّف جو أنّ هذه الدالة ستعيد سلسلةً نصيةً وخطأً بهذا الترتيب، ويمكنك تشغيل البرنامج التالي لرؤية الخرج من هذه الدالة التي تُعيد قيمتين وهما سلسلة نصية وخطأ: package main import ( "errors" "fmt" "strings" ) func capitalize(name string) (string, error) { if name == "" { return "", errors.New("no name provided") } return strings.ToTitle(name), nil } func main() { name, err := capitalize("sammy") if err != nil { fmt.Println("Could not capitalize:", err) return } fmt.Println("Capitalized name:", name) } يكون الخرج كما يلي: Capitalized name: SAMMY عرّفنا الدالة ()capitalize التي تأخذ سلسلة نصية على أساس وسيط وهي السلسلة التي نريد تحويل محارفها إلى محارف كبيرة، وتُعيد سلسلةً نصيةً وقيمة خطأ. استدعينا في الدالة الرئيسية ()main الدالة ()capitalize وأسندنا القيم التي تُعيدها إلى المتغيرين name و err من خلال الفصل بينهما بفاصلة، ثم استخدمنا التعليمة الشرطية if err != nil للتحقق من وجود خطأ والذي سنطبعه في حال وجوده ونخرج من خلال التعليمة return وإلا سنكمل في المسار الطبيعي ونطبع (fmt.Println("Capitalized name:", name. مرَّرنا في المثال السابق الكلمة sammy للدالة ()capitalize، لكن إذا حاولت تمرير سلسلة فارغة ""، فستحصل مباشرةً على رسالة الخطأ Could not capitalize: no name provided، أي عند تمرير سلسلة فارغة، ستُعيد الدالة خطأً، وعند تمرير سلسلة عادية، ستستخدِم الدالة ()capitalize الدالة strings.ToTitle لتحويل السلسلة الممرَّرة إلى محارف كبيرة ثم تُعيدها، كما تُعيد في هذه الحالة nil أيضًا لإشارة إلى عدم وجود خطأ. هناك بعض الاصطلاحات الدقيقة التي اتبعها هذا المثال والتي تُعَدّ نموذجيةً في شيفرات جو ولكن ليست إجباريةً من قِبَل مصرِّف جو، فعندما تكون لدينا دالة تُعيد عدة قيم مثلًا، يُشاع أن تكون قيمة الخطأ المُعادة هي القيمة الأخيرة، أيضًا عندما تُعاد قيمة خطأ ما، فستُسنَد القيمة الصفرية إلى كل قيمة لا تمثّل خطأ، والقيم الصفرية مثلًا هي القيمة 0 في حالة الأعداد الصحيحة أو السلسلة الفارغة في حالة السلاسل النصية أو السجل الفارغ في حالة نوع البيانات struct أو القيمة nil في حالة المؤشر والواجهة interface، وقد تحدّثنا عن ذلك سابقًا بالتفصيل في مقال المتغيرات والثوابت. تقليل استخدام الشيفرة المتداولة يمكن أن يصبح الالتزام بهذه الاصطلاحات مملًا في المواقف التي تكون لدينا فيها العديد من القيم المُعادة من دالة، إذ يمكننا استخدام مفهوم الدالة مجهولة الاسم anonymous function للمساعدة في تقليل الشيفرة المتداولة boilerplate. تُعَدّ الدوال مجهولة الاسم إجرائيات مسنَدَة إلى المتغيرات، على عكس الدوال التي عرَّفناها في الأمثلة السابقة، فهي متوفرة فقط ضمن الدوال التي تُصرّح عنها، وهذا يجعلها مثاليةً لتعمل على أساس أجزاء منطقية قصيرة مُساعدة وقابلة لإعادة الاستخدام. سنعدّل المثال السابق بحيث نضيف له طول الاسم الذي نريد تحويل حالة المحارف فيه إلى الحالة الكبيرة، وبما أنه لدينا ثلاث قيم تجب إعادتها، فقد يصبح التعامل مع الأخطاء مرهقًا بدون دالة مجهولة الاسم تُساعدنا: package main import ( "errors" "fmt" "strings" ) func capitalize(name string) (string, int, error) { handle := func(err error) (string, int, error) { return "", 0, err } if name == "" { return handle(errors.New("no name provided")) } return strings.ToTitle(name), len(name), nil } func main() { name, size, err := capitalize("sammy") if err != nil { fmt.Println("An error occurred:", err) } fmt.Printf("Capitalized name: %s, length: %d", name, size) } يكون الخرج كما يلي: Capitalized name: SAMMY, length: 5 نستقبل 3 وسائط مُعادة من الدالة ()capitalize داخل الدالة ()main، وهي name و size و err على التوالي، ثم نختبر بعد ذلك فيما إذا كانت الدالة ()capitalize قد أعادت خطأً أم لا وذلك من خلال فحص قيمة المتغير err إذا كان nil أم لا، فمن المهم فعل ذلك قبل محاولة استخدام أيّ من القيم الأخرى المُعادة من الدالة capitalize لأن الدالة مجهولة الاسم handle يمكن أن تضبطها على قيم صفرية، وفي هذا المثال لم نُمرِّر سلسلةً فارغةً، لذا أكمل البرنامج عمله وفقًا للمسار الطبيعي، ويمكنك تمرير سلسلة فارغة لترى أنّ الخرج سيكون رسالة خطأ An error occurred: no name provided. عرّفنا المتغير handle داخل الدالة capitalize وأسندنا إليه دالة مجهولة الاسم، أي أنّ هذا المتغير أصبح يُمثّل دالةً مجهولة الاسم، وستأخذ خطأ error على أساس وسيط وتُعيد قيمًا تُطابق القيم التي تُعيدها الدالة capitalize وبالترتيب نفسه لكن تجعل قيمها صفريّة، كما تُعيد الدالة الخطأ الذي مرِّر لها أيضًا كما هو، وبناءً على ذلك سيكون بإمكاننا إعادة أي أخطاء تحدث في الدالة capitalize باستخدام تعليمة return يليها استدعاء الدالة مجهولة الاسم handle مع تمرير الخطأ على أساس وسيط. تذكَّر أنّ الدالة capitalize يجب أن تُعيد دومًا ثلاث قيم فهكذا عرّفناها، ولكن في بعض الأحيان لا نريد التعامل مع جميع القيم التي يمكن أن تُعيدها الدالة، لذا لحسن الحظ يمكننا التعامل مع هذا الأمر من خلال استخدام الشَرطة السفلية _ كما سترى بعد قليل. معالجة الأخطاء في الدوال التي تعيد عدة قيم عندما تُعيد الدالة العديد من القيم، ينبغي علينا إسناد كل منها إلى متغير وهذا ما فعلناه في المثال السابق مع الدالة capitalize، كما يجب فصل هذه المتغيرات بفواصل. لا نحتاج في بعض الأحيان إلا لقيم محددة منها، فقد لا نحتاج مثلًا إلا لقيمة الخطأ error، ففي هذه الحالة يمكنك استخدام الشرطة السفلية _ لتجاهل القيم الأُخرى المُعادة، وقد عدّلنا المثال الأول عن الدالة ()capitalize في المثال التالي ومرّرنا لها سلسلةً فارغةً لكي تُعطينا خطأً واستخدمنا الشرطة السفلية لتجاهل القيمة الأولى التي تُعيدها الدالة كما يلي: package main import ( "errors" "fmt" "strings" ) func capitalize(name string) (string, error) { if name == "" { return "", errors.New("no name provided") } return strings.ToTitle(name), nil } func main() { _, err := capitalize("") if err != nil { fmt.Println("Could not capitalize:", err) return } fmt.Println("Success!") } يكون الخرج كما يلي: Could not capitalize: no name provided استدعينا الدالة ()capitalize في المثال أعلاه داخل الدالة الرئيسية ()main وأسندنا القيم المُعادة منها إلى المتغير _ والمتغير err على التوالي، وبذلك نكون قد تجاهلنا القيمة الأولى المُعادة من الدالة واحتفظنا بقيمة الخطأ داخل المتغير err، وما تبقى شرحناه سابقًا. تعريف أنواع أخطاء جديدة مخصصة نحتاج في بعض الأوقات إلى تعريف أنواع أكثر تعقيدًا من الأخطاء من خلال تنفيذ الواجهة error، إذ لا تكفينا دوال المكتبة القياسية ()errors.New و ()fmt.Errorf في بعض الأحيان لالتقاط ما حدث والإبلاغ عنه بالطريقة المناسبة، لذا تكون البنية التي نحتاج إلى تحقيقها كما يلي: type error interface { Error() string } تتضمّن الواجهة error تابعًا وحيدًا هو ()Error والذي يُعيد سلسلةً نصيةً تمثّل رسالة خطأ، فبهذا التابع ستتمكن من تعريف الخطأ بالطريقة التي تناسبك، وفي المثال التالي سننفِّذ الواجهة error كما يلي: package main import ( "fmt" "os" ) type MyError struct{} func (m *MyError) Error() string { return "boom" } func sayHello() (string, error) { return "", &MyError{} } func main() { s, err := sayHello() if err != nil { fmt.Println("unexpected error: err:", err) os.Exit(1) } fmt.Println("The string:", s) } سنرى الخرج التالي: unexpected error: err: boom exit status 1 عرّفنا نوع بيانات عبارة عن سجل struct فارغ واسميناه MyError، كما عرّفنا التابع ()Error داخله بحيث يُعيد الرسالة "boom". نستدعي الدالة sayHello داخل الدالة الرئيسية ()main التي تُعيد سلسلةً فارغةً ونسخةً جديدةً من MyError، وبما أنّ sayHello ستُعطي خطأً دومًا، فسيُنفَّذ استدعاء ()fmt.Println الموجود داخل التعليمة الشرطية دومًا وستُطبع رسالة الخطأ. لاحظ أنه لا نحتاج إلى استدعاء التابع ()Error مُباشرةً لأن الحزمة fmt قادرة تلقائيًا على اكتشاف أنّ هذا تنفيذ للواجهة error، وبالتالي يُستدعى التابع ()Error تلقائيًّا وتُطبع رسالة الخطأ. الحصول على معلومات تفصيلية عن خطأ يكون الخطأ المخصص custom error عادةً هو أفضل طريقة لالتقاط معلومات تفصيلية عن خطأ، فلنفترض مثلًا أننا نريد التقاط رمز الحالة status code عند حدوث أخطاء ناتجة عن طلب HTTP، لذا سننفِّذ الواجهة error في البرنامج التالي بحيث يمكننا التقاط هكذا معلومات: package main import ( "errors" "fmt" "os" ) type RequestError struct { StatusCode int Err error } func (r *RequestError) Error() string { return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err) } func doRequest() error { return &RequestError{ StatusCode: 503, Err: errors.New("unavailable"), } } func main() { err := doRequest() if err != nil { fmt.Println(err) os.Exit(1) } fmt.Println("success!") } سنرى الخرج التالي: status 503: err unavailable exit status 1 أنشأنا في هذا المثال نسخةً من RequestError وزوّدناها برمز الحالة والخطأ باستخدام الدالة errors.New من المكتبة القياسية، ثم نستخدِم بعد ذلك الدالة ()fmt.Println لطباعتها كما هو الحال في الأمثلة السابقة. استخدمنا الدالة ()fmt.Sprintf داخل التابع ()Error في RequestError لإنشاء سلسلة باستخدام المعلومات المقدَّمة عند إنشاء الخطأ. توكيدات النوع والأخطاء المخصصة تعرض الواجهة error دالةً واحدةً فقط، لكن قد نحتاج إلى الوصول إلى دوال أخرى من تنفيذات أخرى للواجهة error لمعالجة الخطأ بطريقة مناسبة، فقد يكون لدينا مثلًا العديد من التنفيذات للواجهة error والتي تكون مؤقتةً ويمكن إعادة طلبها، إذ يُشار إليها بوجود التابع ()Temporary. توفِّر الواجهات رؤيةً ضيقةً لمجموعة أوسع من التوابع التي يمكن للأنواع أن توفرها، لذلك يجب علينا تطبيق عملية توكيد النوع type assertion لتغيير التوابع التي تُعرض أو إزالتها بالكامل، ويوسِّع المثال التالي النوع RequestError بتضمينه التابع ()Temporary والذي سيشير إذا كان يجب على من يستدعي الدالة إعادة محاولة الطلب أم لا: package main import ( "errors" "fmt" "net/http" "os" ) type RequestError struct { StatusCode int Err error } func (r *RequestError) Error() string { return r.Err.Error() } func (r *RequestError) Temporary() bool { return r.StatusCode == http.StatusServiceUnavailable // 503 } func doRequest() error { return &RequestError{ StatusCode: 503, Err: errors.New("unavailable"), } } func main() { err := doRequest() if err != nil { fmt.Println(err) re, ok := err.(*RequestError) if ok { if re.Temporary() { fmt.Println("This request can be tried again") } else { fmt.Println("This request cannot be tried again") } } os.Exit(1) } fmt.Println("success!") } يكون الخرج كما يلي: unavailable This request can be tried again exit status 1 نستدعي الدالة ()doRequest ضمن الدالة main التي تُعيد لنا الواجهة error، إذ نطبع أولًا رسالة الخطأ المُعادة من التابع ()Error ثم نحاول كشف جميع التوابع من ()RequestError باستخدام توكيد النوع (re, ok := err.(*RequestError، فإذا نجح توكيد النوع، فإننا سنستخدِم التابع ()Temporary لمعرفة ما إذا كان هذا الخطأ خطأً مؤقتًا. بما أنّ المتغير StatusCode الذي هُيئ من خلال الدالة ()doRequest يحمل القيمة 503 والذي يتطابق مع http.StatusServiceUnavailable، فإنّ ذلك يُعيد true وبالتالي طباعة "This request can be tried again"، كما يمكننا من الناحية العملية تقديم طلب آخر بدلًا من طباعة رسالة. تغليف الأخطاء سيكون منشأ الخطأ غالبًا خارجيًا أي من خارج برنامجك مثل قاعدة بيانات واتصال بالشبكة ومدخلات مستخدِم غير صحيحة، …إلخ، فرسائل الخطأ المقدَّمة من هذه الأخطاء لا تساعد أيّ شخص في العثور على أصل الخطأ. سيُقدِّم تغليف الأخطاء بمعلومات إضافية في بداية رسالة الخطأ رؤيةً أفضل (معلومات عن السياق أو الطبيعة التي حدث فيها) لتصحيح الأخطاء بنجاح، ويوضِّح المثال التالي كيف يمكننا إرفاق بعض المعلومات السياقية لخطأ خفي من دالة أخرى: package main import ( "errors" "fmt" ) type WrappedError struct { Context string Err error } func (w *WrappedError) Error() string { return fmt.Sprintf("%s: %v", w.Context, w.Err) } func Wrap(err error, info string) *WrappedError { return &WrappedError{ Context: info, Err: err, } } func main() { err := errors.New("boom!") err = Wrap(err, "main") fmt.Println(err) } يكون الخرج كما يلي: main: boom! يحتوي السجل WrappedError على حقلين هما رسالة عن السياق على هيئة سلسلة نصية والخطأ الذي يقدِّم عنه معلومات إضافيةً، فعندما يُستدعى التابع ()Error، فإننا نستخدِم ()fmt.Sprintf مرةً أخرى لطباعة رسالة السياق ثم الخطأ، إذ يستدعي ()fmt.Sprintf التابع ()Error ضمنيًا. نستدعي الدالة errors.New داخل الدالة ()main ثم نغلِّف الخطأ باستخدام الدالة Wrap التي عرّفناها، إذ يسمح لنا ذلك بالإشارة إلى أنّ هذا الخطأ قد أُنشئ في الدالة main، وبما أنّ WrappedError هي خطأ، لذا يمكننا تغليف العديد منها، إذ يسمح لنا ذلك بالحصول على سلسلة تمكننا من تتبع مصدر الخطأ، كما يمكننا أيضًا تضمين كامل مسار المكدس في الأخطاء التي تحدث مع القليل من المساعدة من المكتبة القياسية. بما أن الواجهة error لا تحتوي إلا تابعًا واحدًا، فسيمنحنا ذلك مرونةً كبيرةً في تقديم أنواع مختلفة من الأخطاء لمواقف مختلفة، ويمكن أن يشمل ذلك كل شيء بدءًا من وصل أجزاء متعددة من المعلومات على أساس جزء من الخطأ الأساسي وصولًا إلى تحقيق التراجع الأسي Exponential backoff. الخاتمة رأينا في هذا المقال العديد من الطرق لإنشاء الأخطاء باستخدام المكتبة القياسية وكيفية إنشاء دوال تُعيد الأخطاء بطريقة اصطلاحية، وتمكَّنا أيضًا من إنشاء العديد من الأخطاء بنجاح باستخدام دوال المكتبة القياسية ()errors.New و ()fmt.Errorf، كما تعلّمت أيضًا كيفية إنشاء أنواع أخطاء مُخصصة وكيفية تتبع الأخطاء التي تحدث في البرنامج من خلال تغليفها. وبطبيعة الحال، الأخطاء البرمجية موجودة وشائعة في مجال البرمجة ولغات البرمجة عمومًا، لذا من أجل التعرف على الأخطاء البرمجية عامةً والتعرف على كيفية التعامل معها، ندعوك لمشاهدة الفيديو الأتي: ترجمة -وبتصرف- للمقال Handling Errors in Go وللمقال Creating Custom Errors in Go لصاحبه Gopher Guides. اقرأ أيضًا المقال السابق: المصفوفات Arrays والشرائح Slices في جو Go كيفية التعامل مع الأخطاء البرمجية الأخطاء السبع القاتلة لأي مشروع برمجيات استخدام المتغيرات والثوابت في لغة جو Go
×
×
  • أضف...