الواجهة مجموعة توابع
يسرد نوع الواجهة (interface) توقيعات توابع. أي نوع له تلك التوابع يحقّق الواجهة، دون أي تصريح يقول ذلك.
لا يذكر Rect ولا Circle الواجهة Shape. هذا هو التطبيق الضمني، الميزة المميّزة لواجهات Go. ويعني أنك تستطيع تعريف واجهة في حزمتك تحقّقها أصلًا أنواع من حزم أخرى، دون المساس بشيفرتها.
واجهات صغيرة من المكتبة القياسية
تفضّل شيفرة Go الواجهات ذات التابع أو التابعين. أهمها:
| الواجهة | التابع | يستخدمها |
|---|---|---|
fmt.Stringer | String() string | الطباعة في fmt |
error | Error() string | كل دالة قد تفشل |
io.Reader | Read(p []byte) (n int, err error) | الملفات، الشبكة، gzip، أجسام HTTP |
io.Writer | Write(p []byte) (n int, err error) | الملفات، المخازن المؤقّتة، دوال التجزئة، ردود HTTP |
sort.Interface | Len وLess وSwap | الحزمة sort |
http.Handler | ServeHTTP(w, r) | net/http |
لأن لـ io.Reader تابعًا واحدًا، تطبّقها عشرات الأنواع، وأي دالة تأخذ io.Reader تعمل معها جميعًا:
يقول مثل Go: "كلما كبرت الواجهة ضعف التجريد". الواجهات الأكبر تُبنى بتركيب الصغيرة: io.ReadWriter هي Reader زائد Writer، وتُكتب بتضمين واجهة في أخرى.
any: الواجهة الفارغة
ليس لـ interface{} أي توابع، فكل نوع يحقّقها. أضافت Go 1.18 الاسم البديل any؛ وهما متطابقان.
قيمة any تحمل أي شيء لكنك لا تستطيع فعل شيء تقريبًا بها حتى تستعيد النوع المحدّد بـ تأكيد النوع أو type switch. فضّل واجهة حقيقية أو الأنواع العامة عندما تكون مجموعة الأنواع معروفة. تناسب any البيانات الديناميكية فعلًا مثل JSON المفكوك ذي الشكل المجهول، والطباعة.
ما الذي تحتويه قيمة الواجهة
قيمة الواجهة زوج: نوع ديناميكي وقيمة ديناميكية. تخزّن var s Shape = Rect{3, 4} النوع Rect ونسخة من القيمة. واستدعاء s.Area() يبحث عن تابع Rect وقت التشغيل.
الواجهة تساوي nil فقط عندما يكون الجزآن فارغين. وهذه القاعدة تسبّب أكثر الأخطاء إرباكًا في Go.
فخ الواجهة nil
المؤشر nil المخزّن في واجهة يصنع واجهة ليست nil.
المخرجات:
false
*main.MyError true
true
تعيد validate(true) واجهة error تحمل النوع *MyError والقيمة nil. للواجهة نوع، فلا تساوي nil، وينفَّذ فرع if err != nil عند المستدعي. واستدعاء err.Error() هناك سيسبّب panic عند الوصول إلى حقل المستقبِل nil.
الإصلاح بسيط: عرّف المتغيّر بالنوع error لا بنوع المؤشر المحدّد، أو أعد nil حرفيًا في مسار النجاح. لا تعد أبدًا نوع مؤشر خطأ محدّدًا من دالة نتيجتها error. والفخ نفسه ينطبق على أي واجهة، لا على الأخطاء وحدها.
التحقّق من أن نوعًا يطبّق واجهة
يُفحص التطبيق حيث تُسند قيمة إلى واجهة. إذا لم تفعل أي شيفرة ذلك بعد، يمرّ خطأ في توقيع تابع دون أن يلاحظه أحد. الإسناد الفارغ على مستوى الحزمة يجعل الفحص صريحًا:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
لا يكلّف هذا شيئًا وقت التشغيل. إذا كان لـ *LogWriter التابع Write(p []byte) error بدل Write(p []byte) (int, error)، يفشل البناء:
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
مستقبِلات المؤشر والواجهات
إذا كان للتابع مستقبِل مؤشر، فنوع المؤشر وحده يملك ذلك التابع. يحقّق *Counter الواجهة عبره؛ ولا يحقّقها Counter. يقول المترجم Counter does not implement Incrementer (method Inc has pointer receiver). خزّن &Counter{} في الواجهة. صفحة التوابع تشرح مجموعات التوابع.
اقبل الواجهات، وأعد البنى
إرشاد شائع في Go: يجب أن تأخذ الدوال معاملات من أنواع واجهات وتعيد أنواعًا محدّدة.
- قبول واجهة يتيح للمستدعين تمرير أي شيء يناسب، بما في ذلك البدائل الوهمية للاختبار. الدالة التي تقرأ بيانات يجب أن تأخذ
io.Readerلا*os.File. - إعادة نوع محدّد تتيح للمستدعين استخدام كل توابعه وحقوله، وتتجنّب فخ الواجهة nil. تعيد
os.Openالنوع*os.Fileلاio.Reader.
عادة مرتبطة: عرّف الواجهات حيث تُستخدم، لا حيث تُطبَّق. إذا احتاجت خدمتك إلى شيء يستطيع Get(id) لمستخدم، فعرّف واجهة بتابع واحد في حزمة خدمتك، ودع حزمة قاعدة البيانات تصدّر بنيتها فقط.
مقارنة قيم الواجهات
تتساوى قيمتا واجهة عندما يتطابق نوعاهما الديناميكيان وتتساوى قيمتاهما الديناميكيتان. إذا كان النوع الديناميكي غير قابل للمقارنة (شريحة أو خريطة)، تُترجم == لكنها تسبّب panic وقت التشغيل: runtime error: comparing uncomparable type []int.
أخطاء شائعة
- إعادة مؤشر nil ذي نوع كواجهة. أعد
nilحرفيًا. - الواجهات المبكرة. اكتب النوع المحدّد أولًا. أضف واجهة عندما يحتاجها تطبيق ثانٍ أو اختبار.
- مؤشر إلى واجهة.
*io.Readerلا يكون صحيحًا تقريبًا أبدًا. الواجهة تحمل مؤشرًا أصلًا عندما تخزّن مؤشرًا فيها. - الواجهات الكبيرة. الواجهات ذات العشرة توابع صعبة التطبيق وصعبة المحاكاة. قسّمها.
الأسئلة الشائعة
كيف تطبّق واجهة في Go؟
عرّف على نوعك التوابع التي تسردها الواجهة، بالأسماء والتوقيعات نفسها. لا توجد كلمة implements. إذا كان لـ *File التابع Read(p []byte) (int, error) فهو io.Reader تلقائيًا. يتحقّق المترجم من ذلك في أي مكان تسند فيه القيمة إلى نوع الواجهة.
ما هي الواجهة الفارغة أو any في Go؟
ليس لـ interface{} أي توابع، فكل نوع يحقّقها. ومنذ Go 1.18 صار any اسمًا بديلًا مدمجًا لـ interface{}. قيمة من النوع any تحمل أي شيء، لكنك تحتاج تأكيد نوع أو type switch لاستخراج نوع محدّد منها.
لماذا لا تكون واجهتي في Go مساوية لـ nil رغم أني أسندت مؤشرًا nil؟
قيمة الواجهة تحمل نوعًا وقيمة. إسناد *MyError قيمته nil إلى error يعطي واجهة نوعها *MyError وقيمتها nil، وهذه الواجهة لا تساوي nil. أعد nil حرفيًا بدل مؤشر nil ذي نوع عندما لا يوجد خطأ.
كيف أتحقّق وقت الترجمة من أن نوعًا يطبّق واجهة؟
أضف إسنادًا فارغًا على مستوى الحزمة: var _ io.Reader = (*MyReader)(nil). إذا نقص *MyReader تابع، يفشل البناء برسالة تسمّي التابع الناقص.