فهم وضوح القاربات في Go

مقدمة

عند إنشاء حزمة في Go، الهدف النهائي عادة هو جعل الحزمة متاحة للمطورين الآخرين للاستخدام، سواء في حزم ذات مستوى أعلى أو برامج كاملة. من خلال استيراد الحزمة، يمكن لجزء الكود الخاص بك أن يكون كتلة البناء لأدوات أكثر تعقيدًا. ومع ذلك، فقط بعض الحزم متاحة للاستيراد. يتم تحديد هذا بواسطة قابلية الرؤية للحزمة.

قابلية الرؤية في هذا السياق تعني مساحة الملف التي يمكن من خلالها الرجوع إلى حزمة أو بنية أخرى. على سبيل المثال، إذا عرفنا متغيرًا في دالة، فإن قابلية الرؤية (النطاق) لذلك المتغير تكون فقط داخل الدالة التي تم تعريفه فيها. وبالمثل، إذا قمت بتعريف متغير في حزمة، يمكنك جعله مرئيًا فقط لتلك الحزمة، أو السماح بجعله مرئيًا خارج الحزمة أيضًا.

التحكم بدقة في رؤية الحزمة مهم عند كتابة كود سهل الاستخدام، وخاصة عند النظر في التغييرات المستقبلية التي قد ترغب في إجرائها على حزمتك. إذا كنت بحاجة إلى إصلاح خطأ، تحسين الأداء، أو تغيير الوظيفة، سترغب في إجراء التغيير بطريقة لا تعطل الكود الذي يستخدمه أي شخص آخر لحزمتك. إحدى الطرق لتقليل التغييرات المخربة هي السماح بالوصول فقط إلى أجزاء حزمتك التي تحتاجها لاستخدامها بشكل صحيح. من خلال تقييد الوصول، يمكنك إجراء تغييرات داخلية على حزمتك بأقل فرصة للتأثير على كيفية استخدام المطورين الآخرين لحزمتك.

في هذه المقالة، ستتعلم كيفية التحكم في رؤية الحزمة، بالإضافة إلى كيفية حماية أجزاء من الكود الخاص بك التي يجب استخدامها فقط داخل حزمتك. للقيام بذلك، سنقوم بإنشاء مسجل أساسي لتسجيل وتصحيح الرسائل، باستخدام حزم بدرجات مختلفة من رؤية العناصر.

المتطلبات الأساسية

لمتابعة الأمثلة في هذه المقالة، ستحتاج إلى:

.
├── bin 
│ 
└── src
    └── github.com
        └── gopherguides

العناصر المصدرة وغير المصدرة

على عكس لغات البرمجة الأخرى مثل Java وPython التي تستخدم معدّلات الوصول مثل public، private، أو protected لتحديد النطاق، يحدد Go ما إذا كان العنصر مصدر وغير مصدر من خلال كيفية الإعلان عنه. في هذه الحالة، يجعل تصدير عنصر ما مرئيًا خارج الحزمة الحالية. إذا لم يتم تصديره، فإنه يكون مرئيًا وقابلًا للاستخدام فقط داخل الحزمة التي تم تعريفه فيها.

يتم التحكم في هذا الرؤية الخارجية من خلال تكبير الحرف الأول للعنصر المعلن. جميع التعريفات، مثل الأنواع، المتغيرات، الثوابت، الدوال، إلخ، التي تبدأ بحرف كبير هي مرئية خارج الحزمة الحالية.

دعونا ننظر إلى الكود التالي، مع الانتباه الكبير للأحرف الكبيرة:

greet.go
package greet

import "fmt"

var Greeting string

func Hello(name string) string {
	return fmt.Sprintf(Greeting, name)
}

يعلن هذا الكود أنه ينتمي إلى الحزمة `greet`. ثم يعلن عن رمزين، متغير يسمى `Greeting`، ودالة تسمى `Hello`. نظرًا لأنهما يبدأان بحرف كبير، فكلاهما `exported` ومتاح لأي برنامج خارجي. كما ذكرنا سابقًا، صياغة حزمة تقيد الوصول ستسمح بتصميم API أفضل وتسهيل تحديث حزمتك داخليًا دون كسر أي كود يعتمد على حزمتك.

تحديد رؤية الحزمة

للإطلاع على كيفية عمل رؤية الحزمة في برنامج، لننشئ حزمة `logging`، مع الأخذ في الاعتبار ما نريد جعله مرئيًا خارج حزمتنا وما لن نجعله مرئيًا. ستكون هذه الحزمة التسجيل مسؤولة عن تسجيل أي رسائل برنامجنا إلى وحدة التحكم. ستتحقق أيضًا من مستوى `level` الذي نقوم بتسجيله عليه. المستوى يصف نوع السجل، وسيكون أحد ثلاثة حالات: `info`، `warning`، أو `error`.

أولاً، داخل دليل `src`، لننشئ دليلًا يسمى `logging` لوضع ملفات التسجيل فيه:

  1. mkdir logging

انتقل إلى هذا الدليل بعد ذلك:

  1. cd logging

ثم، باستخدام محرر مثل nano، قم بإنشاء ملف يسمى `logging.go`:

  1. nano logging.go

ضع الكود التالي في ملف `logging.go` الذي أنشأناه للتو:

logging/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. في هذه الحزمة، هناك وظيفتان exported: Debug و Log. يمكن استدعاء هذه الوظائف من أي حزمة أخرى تستورد حزمة logging. هناك أيضًا متغير خاص يسمى debug. هذا المتغير يمكن الوصول إليه فقط من داخل حزمة logging. من المهم ملاحظة أنه بينما تشترك الوظيفة Debug والمتغير debug في نفس التهجئة، فإن الوظيفة تكتب بأحرف كبيرة والمتغير لا. هذا يجعلهما تعريفين مختلفين بنطاقات مختلفة.

احفظ وأغلق الملف.

لاستخدام هذه الحزمة في أماكن أخرى من الكود، يمكننا استيرادها إلى حزمة جديدة. سننشئ هذه الحزمة الجديدة، لكننا سنحتاج أولاً إلى دليل جديد لتخزين ملفات المصدر فيه.

دعونا نترك دليل logging، وننشئ دليلاً جديدًا يسمى cmd، وننتقل إلى هذا الدليل الجديد:

  1. cd ..
  2. mkdir cmd
  3. cd cmd

أنشئ ملفًا يسمى main.go في دليل cmd الذي أنشأناه للتو:

  1. nano main.go

الآن يمكننا إضافة الكود التالي:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

	logging.Log("This is a debug statement...")
}

لدينا الآن برنامجنا بأكمله مكتوب. ومع ذلك، قبل أن نتمكن من تشغيل هذا البرنامج، سنحتاج أيضًا إلى إنشاء بعض ملفات التكوين لكي يعمل الكود بشكل صحيح. يستخدم Go Go Modules لتكوين تبعيات الحزم لاستيراد الموارد. تعتبر وحدات Go ملفات التكوين الموجودة في دليل الحزم الخاص بك والتي تخبر المترجم من أين يمكن استيراد الحزم. بينما يتجاوز تعلم الوحدات نطاق هذه المقالة، يمكننا كتابة بضعة أسطر من التكوين لجعل هذا المثال يعمل محليًا.

افتح ملف go.mod التالي في دليل cmd:

  1. nano go.mod

ثم ضع المحتويات التالية في الملف:

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. لنعد إلى دليل logging وننشئ ملف go.mod:

  1. cd ../logging
  2. nano go.mod

أضف المحتويات التالية إلى الملف:

go.mod
module github.com/gopherguides/logging

هذا يخبر المترجم أن حزمة logging التي أنشأناها هي في الواقع حزمة github.com/gopherguides/logging. هذا يجعل من الممكن استيراد الحزمة في حزمة main باستخدام السطر التالي الذي كتبناه سابقًا:

cmd/main.go
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 باستخدام الأوامر التالية:

  1. cd ../cmd
  2. go run main.go

ستحصل على ناتج مشابه لما يلي:

Output
2019-08-28T11:36:09-05:00 This is a debug statement...

سيطبع البرنامج الوقت الحالي بتنسيق RFC 3339، متبوعًا بأي بيان أرسلناه إلى المُسجّل. RFC 3339 هو تنسيق زمني تم تصميمه لتمثيل الوقت على الإنترنت ويستخدم بشكل شائع في ملفات السجل.

نظرًا لأن الدوال Debug وLog تُصدر من حزمة التسجيل، يمكننا استخدامها في حزمتنا main. ومع ذلك، لا يتم تصدير المتغير debug في حزمة التسجيل. محاولة الإشارة إلى تعريف غير مُصدر ستؤدي إلى خطأ في وقت الترجمة.

أضف السطر المميز التالي إلى main.go:

cmd/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)
}

احفظ وقم بتشغيل الملف. ستتلقى خطأً مشابهًا لما يلي:

Output
. . . ./main.go:10:14: cannot refer to unexported name logging.debug

الآن وبعد أن رأينا كيفية تصرف العناصر exported وunexported في الحزم، سننظر بعد ذلك في كيفية تصدير fields وmethods من structs.

الرؤية داخل الهياكل

بينما قد تعمل مخطط الرؤية في المُسجّل الذي بنيناه في القسم السابق لبرامج بسيطة، إلا أنه يشارك حالة كبيرة جدًا ليكون مفيدًا من داخل عدة حزم. وذلك لأن المتغيرات المصدرة يمكن لعدة حزم الوصول إليها وتعديلها إلى حالات متناقضة. فتسمح لحالة حزمتك بالتغير بهذه الطريقة تجعل من الصعب توقع كيف سيتصرف برنامجك. على سبيل المثال، مع التصميم الحالي، يمكن لحزمة واحدة تعيين متغير Debug إلى true، وأخرى قد تعينه إلى false في نفس المثيل. وهذا سيخلق مشكلة لأن كلا الحزمتين اللتين تستوردان حزمة logging ستتأثر.

يمكننا جعل المُسجّل معزولًا من خلال إنشاء هيكل ومن ثم تعليق الأساليب عليه. سيسمح لنا ذلك بإنشاء مثيل من المُسجّل ليتم استخدامه بشكل مستقل في كل حزمة تستهلكه.

قم بتغيير حزمة logging إلى ما يلي لإعادة هيكلة الكود وعزل المُسجّل:

logging/logging.go
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. هذا الهيكل سيستضيف حالتنا غير المصدرة، بما في ذلك تنسيق الوقت المراد طباعته والمتغير debug الذي يتم تعيينه إلى true أو false. تقوم الدالة New بتعيين الحالة الأولية لإنشاء المسجل بها، مثل تنسيق الوقت وحالة التصحيح. ثم تخزن القيم التي أعطيناها داخليًا في المتغيرات غير المصدرة timeFormat و debug. كما أنشأنا أيضًا طريقة تسمى Log على نوع Logger الذي يأخذ بيانًا نريد طباعته. داخل طريقة Log هناك مرجع لمتغير الطريقة المحلي l للوصول إلى الحقول الداخلية مثل l.timeFormat و l.debug.

هذا النهج سيسمح لنا بإنشاء Logger في العديد من الحزم المختلفة واستخدامه بشكل مستقل عن كيفية استخدام الحزم الأخرى له.

لاستخدامه في حزمة أخرى، لنغير cmd/main.go ليبدو كالتالي:

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...")
}

تشغيل هذا البرنامج سيعطيك الناتج التالي:

Output
2019-08-28T11:56:49-05:00 This is a debug statement...

في هذا الكود، أنشأنا مثيل للمسجل باستدعاء الدالة المصدرة New. قمنا بتخزين المرجع لهذا المثيل في المتغير logger. يمكننا الآن استدعاء logging.Log لطباعة البيانات.

إذا حاولنا الوصول إلى حقل غير مصدر من Logger مثل حقل timeFormat، سنتلقى خطأً أثناء التصحيح. جرب إضافة السطر المميز التالي وتشغيل cmd/main.go:

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)
}

هذا سيعطي الخطأ التالي:

Output
. . . cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)

يدرك المترجم أن logger.timeFormat غير مصدر، وبالتالي لا يمكن استرداده من حزمة logging.

الرؤية داخل الأساليب

بنفس الطريقة كما هي الحال مع حقول الهيكل، يمكن أيضًا تصدير الأساليب أو عدم تصديرها.

لتوضيح ذلك، دعونا نضيف تسجيلًا مصنف إلى مُسجلنا. التسجيل المصنف هو وسيلة لتصنيف سجلاتك حتى تتمكن من البحث في سجلاتك عن أنواع معينة من الأحداث. المستويات التي سنضعها في مُسجلنا هي:

  • مستوى info، الذي يمثل أحداث نوع المعلومات التي تُعلم المستخدم بإجراء ما، مثل بدأ البرنامج، أو تم إرسال البريد الإلكتروني. تساعدنا هذه في التصحيح وتتبع أجزاء من برنامجنا لمعرفة ما إذا كان السلوك المتوقع يحدث.

  • مستوى warning. تحدد هذه الأنواع من الأحداث متى يحدث شيء غير متوقع لا يعتبر خطأ، مثل فشل إرسال البريد الإلكتروني، تجربة إعادة. تساعدنا في رؤية أجزاء من برنامجنا لا تسير بسلاسة كما كنا نتوقعها.

  • مستوى `error`، مما يعني أن البرنامج واجه مشكلة، مثل `ملف غير موجود`. غالبًا ما يؤدي هذا إلى فشل تشغيل البرنامج.

قد ترغب أيضًا في تشغيل وإيقاف مستويات تسجيل معينة، خاصة إذا لم يكن أداء برنامجك كما هو متوقع وتود تصحيح الأخطاء في البرنامج. سنضيف هذه الوظيفة عن طريق تغيير البرنامج بحيث عندما يكون `debug` مضبوطًا على `true`، سيتم طباعة جميع مستويات الرسائل. وإلا إذا كان `false`، سيتم طباعة الرسائل الخاطئة فقط.

أضف تسجيل المستويات من خلال إجراء التغييرات التالية على `logging/logging.go`:

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` رسالة السجل. تحدد طريقة `Log` مستوى الرسالة. إذا كانت رسالة `info` أو `warning`، وكان الحقل `debug` `true`، فإنها تكتب الرسالة. وإلا فإنها تتجاهل الرسالة. إذا كان أي مستوى آخر، مثل `error`، سيتم كتابة الرسالة بغض النظر.

معظم المنطق الذي يحدد ما إذا كان سيتم طباعة الرسالة موجود في طريقة Log. لقد قدمنا أيضًا طريقة غير مصدرة تسمى write. طريقة write هي التي تقوم بالفعل بإخراج رسالة السجل.

يمكننا الآن استخدام هذا التسجيل المرتبط في حزمتنا الأخرى عن طريق تغيير cmd/main.go ليبدو كالتالي:

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")

}

تشغيل هذا سيعطيك:

Output
[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

في هذا المثال، استخدم cmd/main.go بنجاح طريقة Log المصدرة.

يمكننا الآن تمرير level لكل رسالة عن طريق تغيير debug إلى false:

main.go
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:

Output
[error] 2019-08-28T13:58:52-05:00 exiting: no work performed

إذا حاولنا استدعاء طريقة write من خارج حزمة logging، سنتلقى خطأً زمنيًا للتصحيح:

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")

	logger.write("error", "log this message...")
}
Output
cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)

عندما يرى المترجم أنك تحاول الإشارة إلى شيء من حزمة أخرى يبدأ بحرف صغير، فإنه يعرف أنه غير مصدر، وبالتالي يلقي خطأً في التصحيح.

مثّل المسجّل في هذا الدرس كيف يمكننا كتابة كود يكشف فقط عن الأجزاء التي نريد أن تستهلكها الحزم الأخرى. نظرًا لأننا نتحكم في الأجزاء التي تكون مرئية خارج الحزمة، فإننا الآن قادرون على إجراء تغييرات مستقبلية دون التأثير على أي كود يعتمد على حزمتنا. على سبيل المثال، إذا أردنا فقط إيقاف تشغيل info رسائل المستوى عندما debug خطأ، يمكنك إجراء هذا التغيير دون التأثير على أي جزء آخر من واجهة برمجة التطبيقات الخاصة بك. يمكننا أيضًا إجراء تغييرات آمنة على رسالة السجل لتضمين معلومات أكثر، مثل الدليل الذي كان يعمل منه البرنامج.

الخاتمة

أظهرت هذه المقالة كيفية مشاركة الكود بين الحزم مع حماية تفاصيل التنفيذ لحزمتك. هذا يتيح لك تصدير واجهة برمجة تطبيقات بسيطة سيتم تغييرها بشكل نادر للتوافق مع الإصدارات السابقة، ولكنها ستسمح بالتغييرات سراً في حزمتك حسب الحاجة لجعلها تعمل بشكل أفضل في المستقبل. يعتبر هذا ممارسة جيدة عند إنشاء الحزم وواجهات برمجة التطبيقات المقابلة لها.

لمعرفة المزيد حول الحزم في Go، تفقد مقالتنا استيراد الحزم في Go و كيفية كتابة الحزم في Go، أو استكشف سلسلتنا بأكملها كيفية البرمجة في Go.

Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go