الهدف من إنشاء الحزم في لغة جو أو في أي لغة أخرى هو جعل هذه الحزمة متاحة للاستخدام وسهلة الوصول في أيّ وقت من قِبَل مطورين آخرين أو حتى نحن، إذ تُستخدَم هذه الحزم ضمن برمجيات محددة أو بوصفها جزءًا من حزم أكثر تعقيدًا أو أعلى مستوى، لكن لايمكن الوصول إلى جميع الحزم من خارج الحزمة نفسها، ويعتمد ذلك على نطاق رؤيتها.
تُعَدّ الرؤية 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.