من مقالة البنى في Go، نعلم أن نوع بنية يمكن أن يمتلك حقولاً كثيرة. يتكوّن كل حقل من اسم حقل ونوع حقل واحد. في الواقع، يمكن أحياناً أن يتكوّن حقل بنية من نوع حقل فقط. تُسمّى طريقة تعريف حقول البنية تضمين الأنواع (type embedding).
ستشرح هذه المقالة الهدف من تضمين الأنواع وجميع أنواع التفاصيل في تضمين الأنواع.
كيف يبدو تضمين الأنواع؟
إليك مثالاً يوضّح تضمين الأنواع:
package main
import "net/http"
func main() {
type P = *bool
type M = map[int]int
var x struct {
string // نوع مُسمّى غير مؤشر
error // نوع واجهة مُسمّى
*int // نوع مؤشر غير مُسمّى
P // اسم مستعار لنوع مؤشر غير مُسمّى
M // اسم مستعار لنوع غير مُسمّى
http.Header // نوع خريطة مُسمّى
}
x.string = "Go"
x.error = nil
x.int = new(int)
x.P = new(bool)
x.M = make(M)
x.Header = http.Header{}
}
في المثال أعلاه، ضُمِّنت ستة أنواع في نوع البنية. ويشكّل كل تضمين نوع حقل بنية مضمَّناً (embedded field).
تُسمّى الحقول المضمَّنة أيضاً الحقول المجهولة (anonymous fields). ومع ذلك، فإن لكل حقل مضمَّن اسماً محدَّداً ضمنياً. ويعمل اسم النوع غير المؤهَّل (unqualified) لحقل مضمَّن كاسم للحقل. على سبيل المثال، أسماء الحقول المضمَّنة الستة في الأمثلة أعلاه هي string وerror وint وP وM وHeader على التوالي.
ما الأنواع التي يمكن تضمينها؟
تقول مواصفة Go الحالية (الإصدار 1.25) ما يلي
يجب تحديد الحقل المضمَّن كاسم نوع
Tأو كمؤشر إلى اسم نوع غير واجهة*T، ولا يجوز أن يكونTنفسه نوع مؤشر.
كان الوصف أعلاه دقيقاً قبل Go 1.9. ولكن مع إدخال الأسماء المستعارة للأنواع (type aliases) في Go 1.9، أصبح هذا الوصف قديماً وغير دقيق بعض الشيء. على سبيل المثال، لا يشمل الوصف حالة الحقل P في مثال القسم السابق.
هنا، تحاول المقالة تقديم أوصاف أكثر دقة.
- يمكن تضمين اسم النوع
Tكحقل مضمَّن ما لم يكنTيدل على نوع مؤشر مُسمّى أو على نوع مؤشر نوعه الأساس (base type) إما مؤشر وإما نوع واجهة. - يمكن تضمين نوع المؤشر
*T، حيثTاسم نوع يدل على النوع الأساس لنوع المؤشر، كحقل مضمَّن ما لم يكن اسم النوعTيدل على نوع مؤشر أو نوع واجهة.
يسرد ما يلي بعض الأمثلة على أنواع يمكن تضمينها وأنواع لا يمكن:
type Encoder interface {Encode([]byte) []byte}
type Person struct {name string; age int}
type Alias = struct {name string; age int}
type AliasPtr = *struct {name string; age int}
type IntPtr *int
type AliasPP = *IntPtr
// يمكن تضمين هذه الأنواع والأسماء المستعارة.
Encoder
Person
*Person
Alias
*Alias
AliasPtr
int
*int
// لا يمكن تضمين هذه الأنواع والأسماء المستعارة.
AliasPP // النوع الأساس نوع مؤشر
*Encoder // النوع الأساس نوع واجهة
*AliasPtr // النوع الأساس نوع مؤشر
IntPtr // نوع مؤشر مُسمّى
*IntPtr // النوع الأساس نوع مؤشر
*chan int // النوع الأساس نوع غير مُسمّى
struct {age int} // نوع غير مُسمّى غير مؤشر
map[string]int // نوع غير مُسمّى غير مؤشر
[]int64 // نوع غير مُسمّى غير مؤشر
func() // نوع غير مُسمّى غير مؤشر
لا يُسمح لحقلين بأن يحملا الاسم نفسه في بنية، ولا يوجد استثناء لذلك في حقول البنية المجهولة. ووفقاً لقواعد تسمية الحقول المضمَّنة، لا يمكن تضمين نوع مؤشر غير مُسمّى مع نوعه الأساس في نوع البنية نفسه. على سبيل المثال، لا يمكن تضمين int و*int في نوع البنية نفسه.
لا يمكن لنوع بنية أن يضمّن نفسه أو أسماءه المستعارة، بشكل تعاودي.
بشكل عام، لا يكون تضمين الأنواع ذا معنى إلا للأنواع التي تمتلك حقولاً أو طرقاً (ستوضّح الأقسام التالية السبب)، مع أنه يمكن أيضاً تضمين بعض الأنواع التي لا تمتلك أي حقل أو طريقة.
ما أهمية تضمين الأنواع؟
الهدف الرئيسي من تضمين الأنواع هو توسيع وظائف الأنواع المضمَّنة إلى النوع المضمِّن، حتى لا نحتاج إلى إعادة تنفيذ وظائف الأنواع المضمَّنة من أجل النوع المضمِّن.
تستخدم لغات برمجة كائنية التوجه أخرى كثيرة الوراثة (inheritance) لتحقيق الهدف نفسه الذي يحققه تضمين الأنواع. ولكل من الآليتين فوائدها ومساوئها. ولن تناقش هذه المقالة هنا أيهما أفضل. ينبغي أن نعرف فقط أن Go اختارت آلية تضمين الأنواع، وأن هناك فرقاً كبيراً بين الآليتين:
- إذا ورث نوع
Tنوعاً آخر، فإن النوعTيكتسب قدرات النوع الآخر. وفي الوقت نفسه، يمكن أيضاً اعتبار كل قيمة من النوعTقيمة من النوع الآخر. - إذا ضمّن نوع
Tنوعاً آخر، فإن النوع الآخر يصبح جزءاً من النوعT، ويكتسب النوعTقدرات النوع الآخر، لكن لا يمكن اعتبار أي من قيم النوعTقيمة من النوع الآخر.
إليك مثالاً يوضّح كيف يوسّع النوع المضمِّن وظائف النوع المضمَّن.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) PrintName() {
fmt.Println("Name:", p.Name)
}
func (p *Person) SetAge(age int) {
p.Age = age
}
type Singer struct {
Person // يوسّع Person بتضمينه
works []string
}
func main() {
var gaga = Singer{Person: Person{"Gaga", 30}}
gaga.PrintName() // Name: Gaga
gaga.Name = "Lady Gaga"
(&gaga).SetAge(31)
(&gaga).PrintName() // Name: Lady Gaga
fmt.Println(gaga.Age) // 31
}
من المثال أعلاه، يبدو أنه بعد تضمين النوع Person، يكتسب النوع Singer جميع طرق وحقول النوع Person، ويكتسب النوع *Singer جميع طرق النوع *Person. فهل هذه الاستنتاجات صحيحة؟ ستجيب الأقسام التالية عن هذا السؤال.
لاحظ أن قيمة Singer ليست قيمة Person، فالشيفرة التالية لا تُصرَّف:
var gaga = Singer{}
var _ Person = gaga
هل يكتسب النوع المضمِّن حقول الأنواع المضمَّنة وطرقها؟
لنسرد جميع حقول وطرق النوع Singer وطرق النوع *Singer المستخدمة في المثال السابق باستخدام وظائف الانعكاس التي توفرها حزمة reflect القياسية.
package main
import (
"fmt"
"reflect"
)
... // الأنواع المعرَّفة في المثال السابق
func main() {
t := reflect.TypeOf(Singer{}) // نوع Singer
fmt.Println(t, "has", t.NumField(), "fields:")
for i := 0; i < t.NumField(); i++ {
fmt.Print(" field#", i, ": ", t.Field(i).Name, "\n")
}
fmt.Println(t, "has", t.NumMethod(), "methods:")
for i := 0; i < t.NumMethod(); i++ {
fmt.Print(" method#", i, ": ", t.Method(i).Name, "\n")
}
pt := reflect.TypeOf(&Singer{}) // نوع *Singer
fmt.Println(pt, "has", pt.NumMethod(), "methods:")
for i := 0; i < pt.NumMethod(); i++ {
fmt.Print(" method#", i, ": ", pt.Method(i).Name, "\n")
}
}
النتيجة:
main.Singer has 2 fields:
field#0: Person
field#1: works
main.Singer has 1 methods:
method#0: PrintName
*main.Singer has 2 methods:
method#0: PrintName
method#1: SetAge
من النتيجة، نعلم أن النوع Singer يمتلك فعلاً طريقة PrintName، وأن النوع *Singer يمتلك فعلاً طريقتين، PrintName وSetAge. لكن النوع Singer لا يمتلك حقل Name. فلماذا يكون تعبير المُحدِّد gaga.Name صالحاً لقيمة gaga من النوع Singer؟ اقرأ القسم التالي لتعرف السبب.
اختصارات المُحدِّدات
من مقالتَي البنى في Go والطرق في Go، تعلمنا أنه بالنسبة لقيمة x، يُسمّى x.y مُحدِّداً (selector)، حيث y إما اسم حقل وإما اسم طريقة. وإذا كان y اسم حقل، فيجب أن تكون x قيمة بنية أو قيمة مؤشر إلى بنية. والمُحدِّد تعبير يمثل قيمة. وإذا كان المُحدِّد x.y يدل على حقل، فقد يمتلك بدوره حقوله الخاصة (إذا كانت x.y قيمة بنية) وطرقه. مثل x.y.z، حيث يمكن أن يكون z أيضاً إما اسم حقل وإما اسم طريقة.
في Go، (دون اعتبار تصادم المُحدِّدات وتظليلها (colliding and shadowing) المشروحين في قسم لاحق)، إذا كان اسم أوسط في مُحدِّد يقابل حقلاً مضمَّناً، فيمكن حذف ذلك الاسم من المُحدِّد. لهذا تُسمّى الحقول المضمَّنة أيضاً حقولاً مجهولة.
على سبيل المثال:
package main
type A struct {
FieldX int
}
func (a A) MethodA() {}
type B struct {
*A
}
type C struct {
B
}
func main() {
var c = &C{B: B{A: &A{FieldX: 5}}}
// الأسطر الأربعة التالية متكافئة.
_ = c.B.A.FieldX
_ = c.B.FieldX
_ = c.A.FieldX // A حقل مُرقّى لـ C
_ = c.FieldX // FieldX حقل مُرقّى
// الأسطر الأربعة التالية متكافئة.
c.B.A.MethodA()
c.B.MethodA()
c.A.MethodA()
c.MethodA() // MethodA طريقة مُرقّاة لـ C
}
لهذا يكون التعبير gaga.Name صالحاً في مثال القسم السابق؛ فهو مجرد اختصار لـ gaga.Person.Name.
وبالمثل، يمكن اعتبار المُحدِّد gaga.PrintName اختصاراً لـ gaga.Person.PrintName. لكن لا بأس أيضاً إن اعتبرناه ليس اختصاراً؛ فالنوع Singer يمتلك فعلاً طريقة PrintName، وإن كانت الطريقة معرَّفة ضمنياً (اقرأ القسم الذي يلي القسم التالي للتفاصيل). ولسبب مشابه، يمكن أيضاً اعتبار المُحدِّدين (&gaga).PrintName و(&gaga).SetAge اختصارين لـ (&gaga.Person).PrintName و(&gaga.Person).SetAge، أو عدم اعتبارهما كذلك.
يُسمّى Name حقلاً مُرقّى (promoted field) للنوع Singer. وتُسمّى PrintName طريقة مُرقّاة (promoted method) للنوع Singer.
لاحظ أننا نستطيع أيضاً استخدام المُحدِّد gaga.SetAge، لكن فقط إذا كانت gaga قيمة قابلة للعنونة (addressable) من النوع Singer. إنه مجرد سكر نحوي (syntactical sugar) لـ (&gaga).SetAge. اقرأ استدعاءات الطرق للتفاصيل.
في الأمثلة أعلاه، يُسمّى c.B.A.FieldX الصيغة الكاملة للمُحدِّدات c.FieldX وc.B.FieldX وc.A.FieldX. وبالمثل، يُسمّى c.B.A.MethodA الصيغة الكاملة للمُحدِّدات c.MethodA وc.B.MethodA وc.A.MethodA.
إذا كان كل اسم أوسط في الصيغة الكاملة لمُحدِّد يقابل حقلاً مضمَّناً، فيُسمّى عدد الأسماء الوسطى في المُحدِّد عمق المُحدِّد. على سبيل المثال، عمق المُحدِّد c.MethodA المستخدم في مثال أعلاه هو 2، لأن الصيغة الكاملة للمُحدِّد هي c.B.A.MethodA.
تظليل المُحدِّدات وتصادمها
بالنسبة لقيمة x (ينبغي أن نفترض دائماً أنها قابلة للعنونة، حتى لو لم تكن كذلك)، من الممكن أن يكون لكثير من صيغها الكاملة للمُحدِّدات العنصر الأخير نفسه y، وأن يمثّل كل اسم أوسط في هذه المُحدِّدات حقلاً مضمَّناً. في مثل هذه الحالات،
- لا يمكن اختصار سوى الصيغة الكاملة للمُحدِّد ذات العمق الأقل (بافتراض أنها الوحيدة) إلى
x.y. وبعبارة أخرى، يدلx.yعلى الصيغة الكاملة للمُحدِّد ذات العمق الأقل. وتُظلَّل بقية الصيغ الكاملة للمُحدِّدات بواسطة ذات العمق الأقل. - إذا وُجد أكثر من صيغة كاملة لمُحدِّد بالعمق الأقل، فلا يمكن اختصار أي منها إلى
x.y. ونقول إن تلك الصيغ الكاملة للمُحدِّدات ذات العمق الأقل تتصادم مع بعضها بعضاً.
إذا ظُلِّل مُحدِّد طريقة بمُحدِّد طريقة آخر، وكانت مواصفتا الطريقتين المتقابلتين متطابقتين، نقول إن الطريقة الأولى مُتجاوَزة (overridden) بواسطة الأخرى.
على سبيل المثال، افترض أن A وB وC ثلاثة أنواع معرَّفة.
type A struct {
x string
}
func (A) y(int) bool {
return false
}
type B struct {
y bool
}
func (B) x(string) {}
type C struct {
B
}
الشيفرة التالية لا تُصرَّف. والسبب أن عمقَي المُحدِّدين v1.A.x وv1.B.x متساويان، لذا يتصادم المُحدِّدان ولا يمكن اختصار أي منهما إلى v1.x. والوضع نفسه ينطبق على المُحدِّدين v1.A.y وv1.B.y.
var v1 struct {
A
B
}
func f1() {
_ = v1.x // خطأ: مُحدِّد غامض v1.x
_ = v1.y // خطأ: مُحدِّد غامض v1.y
}
الشيفرة التالية تُصرَّف بلا مشكلة. فالمُحدِّد v2.C.B.x مُظلَّل بواسطة v2.A.x، لذا فإن المُحدِّد v2.x هو في الواقع صيغة مختصرة لـ v2.A.x. وللسبب نفسه، المُحدِّد v2.y صيغة مختصرة لـ v2.A.y، وليس لـ v2.C.B.y.
var v2 struct {
A
C
}
func f2() {
fmt.Printf("%T \n", v2.x) // string
fmt.Printf("%T \n", v2.y) // func(int) bool
}
المُحدِّدات المتصادمة أو المُظلَّلة لا تمنع ترقية مُحدِّداتها الأعمق. على سبيل المثال، لا يزال المُحدِّدان .M و.z يُرقَّيان في المثال التالي.
package main
type x string
func (x) M() {}
type y struct {
z byte
}
type A struct {
x
}
func (A) y(int) bool {
return false
}
type B struct {
y
}
func (B) x(string) {}
func main() {
var v struct {
A
B
}
//_ = v.x // خطأ: مُحدِّد غامض v.x
//_ = v.y // خطأ: مُحدِّد غامض v.y
_ = v.M // حسن. <=> v.A.x.M
_ = v.z // حسن. <=> v.B.y.z
}
من التفاصيل غير المألوفة التي ينبغي ملاحظتها أن طريقتين غير مُصدَّرتين (أو حقلين) من حزمتين مختلفتين تُعتبران دائماً معرِّفين مختلفين، حتى لو كان اسماهما متطابقين. لذا لن تتصادم أو تُظلِّل بعضها بعضاً أبداً عندما تُضمَّن أنواعها المالكة في نوع البنية نفسه. على سبيل المثال، برنامج يتكوّن من حزمتين كما يلي سيُصرَّف ويعمل بلا مشكلة. لكن إذا استُبدلت جميع حالات m() بـ M()، فسيفشل تصريف البرنامج لأن A.M وB.M يتصادمان مع بعضهما، وبالتالي لا يكون c.M مُحدِّداً صالحاً.
package foo // import "x.y/foo"
import "fmt"
type A struct {
n int
}
func (a A) m() {
fmt.Println("A", a.n)
}
type I interface {
m()
}
func Bar(i I) {
i.m()
}
package main
import "fmt"
import "x.y/foo"
type B struct {
n bool
}
func (b B) m() {
fmt.Println("B", b.n)
}
type C struct{
foo.A
B
}
func main() {
var c C
c.m() // B false
foo.Bar(c) // A 0
}
الطرق الضمنية للأنواع المضمِّنة
كما ذُكر أعلاه، يمتلك كل من النوع Singer والنوع *Singer طريقة PrintName، ويمتلك النوع *Singer أيضاً طريقة SetAge. لكننا لم نعرّف هذه الطرق صراحةً للنوعين أبداً. فمن أين تأتي هذه الطرق؟
في الواقع، افترض أن نوع بنية S يضمّن نوعاً (أو اسم نوع مستعار) T وأن التضمين قانوني،
- لكل طريقة من طرق النوع المضمَّن
T، إذا لم تتصادم المُحدِّدات المؤدية إلى تلك الطريقة مع مُحدِّدات أخرى ولم تُظلَّل بها، فستعلن المصرِّفات ضمناً طريقة مقابلة بالمواصفات نفسها لنوع البنية المضمِّنS. وبالتالي، ستعلن المصرِّفات أيضاً طريقة مقابلة ضمناً لنوع المؤشر*S. - ولكل طريقة من طرق نوع المؤشر
*T، إذا لم تتصادم المُحدِّدات المؤدية إلى تلك الطريقة مع مُحدِّدات أخرى ولم تُظلَّل بها، فستعلن المصرِّفات ضمناً طريقة مقابلة بالمواصفات نفسها لنوع المؤشر*S.
وببساطة،
- يكتسب كل من النوع
struct{Embedded}والنوع*struct{Embedded}(إذا كانا صالحين) جميع طرق النوع الذي يدل عليهEmbedded. - وتكتسب الأنواع
*struct{Embedded}وstruct{*Embedded}و*struct{*Embedded}(إذا كانت صالحة) جميعاً جميع طرق النوع*Embedded.
تُعلن المصرِّفات الطرق (المُرقّاة) التالية ضمناً للنوع Singer والنوع *Singer.
// ملاحظة: هذه التعريفات ليست صيغة Go قانونية.
// وهي معروضة هنا لغرض الشرح فقط.
// إنها توضّح كيف تُقيَّم قيم الطرق الضمنية
// (اقرأ القسم التالي للمزيد).
func (s Singer) PrintName = s.Person.PrintName
func (s *Singer) PrintName = (*s).Person.PrintName
func (s *Singer) SetAge = (&(*s).Person).SetAge
الأجزاء اليمنى هي الصيغ الكاملة المقابلة للمُحدِّدات.
من مقالة الطرق في Go، نعلم أننا لا نستطيع تعريف طرق صراحةً لأنواع البنى غير المُسمّاة وأنواع المؤشرات غير المُسمّاة التي أنواعها الأساس بنى غير مُسمّاة. لكن من خلال تضمين الأنواع، يمكن لهذه الأنواع غير المُسمّاة أن تمتلك طرقاً أيضاً.
إذا ضمّن نوع بنية نوعاً يطبّق نوع واجهة (وقد يكون النوع المضمَّن هو نوع الواجهة نفسه)، فإن نوع البنية يطبّق نوع الواجهة أيضاً عموماً، إلا إذا كانت هناك طريقة تحددها الواجهة مُظلَّلة أو متصادمة مع طرق أو حقول أخرى. على سبيل المثال، في البرنامج المثال أعلاه، يطبّق كل من نوع البنية المضمِّن ونوع المؤشر الذي نوعه الأساس هو نوع البنية المضمِّن نوع الواجهة I.
لاحظ أن النوع لا يكتسب إلا طرق الأنواع التي يضمّنها مباشرةً أو غير مباشرة. وبعبارة أخرى، تتكوّن مجموعة الطرق (method set) لنوع ما من الطرق المعرَّفة مباشرةً (سواء صراحةً أو ضمناً) للنوع، ومن مجموعة الطرق للنوع الكامن (underlying type) لهذا النوع. على سبيل المثال، في الشيفرة التالية،
- لا يمتلك النوع
Ageأي طرق، لأنه لا يضمّن أي أنواع. - يمتلك النوع
Xطريقتين،IsOddوDouble. وقد اكتُسبتIsOddبتضمين النوعMyInt. - لا يمتلك النوع
Yأي طرق، لأن النوعAgeالمضمَّن فيه لا يمتلك طرقاً. - يمتلك النوع
Zطريقة واحدة فقط،IsOdd، وهي مكتسبة من تضمين النوعMyInt. ولا يكتسب الطريقةDoubleمن النوعX، لأنه لا يضمّن النوعX.
type MyInt int
func (mi MyInt) IsOdd() bool {
return mi%2 == 1
}
type Age MyInt
type X struct {
MyInt
}
func (x X) Double() MyInt {
return x.MyInt + x.MyInt
}
type Y struct {
Age
}
type Z X
تطبيع قيم الطرق المُرقّاة وتقييمها
افترض أن v.m تعبير صالح لقيمة طريقة مُرقّاة؛ فستُطبِّعه المصرِّفات كنتيجة لتحويل عمليات أخذ العنوان وإلغاء الإشارة إلى المؤشر الضمنية إلى عمليات صريحة في الصيغة الكاملة المقابلة للمُحدِّد v.m.
وكما في أي تقييم آخر لقيمة طريقة، فإنه بالنسبة إلى تعبير قيمة طريقة مُطبَّع v.m، وفي وقت التشغيل، عندما تُقيَّم قيمة الطريقة v.m، يُقيَّم معطى المستقبِل v وتُحفظ نسخة من نتيجة التقييم وتُستخدم في الاستدعاءات اللاحقة لقيمة الطريقة.
على سبيل المثال، في الشيفرة التالية
- الصيغة الكاملة لمُحدِّد تعبير الطريقة المُرقّاة
s.M1هيs.T.X.M1. وبعد تحويل عمليات أخذ العنوان وإلغاء الإشارة إلى المؤشر الضمنية فيه، تصبح(*s.T).X.M1. وفي وقت التشغيل، يُقيَّم معطى المستقبِل(*s.T).Xوتُحفظ نسخة من نتيجة التقييم وتُستخدم في الاستدعاءات اللاحقة لقيمة الطريقة المُرقّاة. ونتيجة التقييم هي1، ولهذا يطبع الاستدعاءf()دائماً1. - الصيغة الكاملة لمُحدِّد تعبير الطريقة المُرقّاة
s.M2هيs.T.X.M2. وبعد تحويل عمليات أخذ العنوان وإلغاء الإشارة إلى المؤشر الضمنية فيه، تصبح(&(*s.T).X).M2. وفي وقت التشغيل، يُقيَّم معطى المستقبِل&(*s.T).Xوتُحفظ نسخة من نتيجة التقييم وتُستخدم في الاستدعاءات اللاحقة لقيمة الطريقة المُرقّاة. ونتيجة التقييم هي عنوان الحقلs.X(المعروف أيضاً بـ(*s.T).X). وأي تغيير في القيمةs.Xسينعكس من خلال إلغاء الإشارة إلى العنوان، لكن التغييرات في القيمةs.Tليس لها تأثير على نتيجة التقييم، ولهذا يطبع الاستدعاءانg()كلاهما2.
package main
import "fmt"
type X int
func (x X) M1() {
fmt.Println(x)
}
func (x *X) M2() {
fmt.Println(*x)
}
type T struct { X }
type S struct { *T }
func main() {
var t = &T{X: 1}
var s = S{T: t}
var f = s.M1 // <=> (*s.T).X.M1
var g = s.M2 // <=> (&(*s.T).X).M2
s.X = 2
f() // 1
g() // 2
s.T = &T{X: 3}
f() // 1
g() // 2
}
أنواع الواجهات تضمّن جميع أصناف الأنواع
يمكن لأنواع الواجهات تضمين جميع أصناف الأنواع. اقرأ الواجهات في Go للتفاصيل.
مثال شائق على تضمين الأنواع
في الختام، لنطّلع على مثال شائق. البرنامج المثال سيدخل في حلقة لا نهائية وسيحدث تجاوز سعة المكدس (stack overflow). إذا كنت قد فهمت المحتوى أعلاه وتعدد الأشكال (polymorphism) وتضمين الأنواع، فمن السهل أن تفهم لماذا سيدخل في حلقة لا نهائية.
package main
type I interface {
m()
}
type T struct {
I
}
func main() {
var t T
var i = &t
t.I = i
i.m() // سيستدعي t.m()، ثم يستدعي i.m() مرة أخرى، ...
}