هياكل Go
الهيكل يمكنه تخزين مجموعة من البيانات بأنواع مختلفة، وهو نوع مركب. تخلت Go عن الصفوف والوراثة، كما تخلت عن طرق البناء، وأضعفت عمداً وظائف البرمجة كائنية التوجه، Go ليست لغة OOP تقليدية، لكن لا تزال هناك ظل لـ OOP في Go، من خلال الهياكل والطرق يمكن محاكاة فصل. فيما يلي مثال بسيط للهيكل:
type Programmer struct {
Name string
Age int
Job string
Language []string
}التعريف
تعريف الهيكل بسيط جداً، مثال:
type Person struct {
name string
age int
}الهيكل نفسه وحقوله الداخلية تتبع قواعد التسمية بحجم الأحرف للكشف. بالنسبة لبعض الحقول المتجاورة من نفس النوع، لا حاجة لتكرار تعريف النوع، كالتالي:
type Rectangle struct {
height, width, area int
color string
}TIP
عند تعريف حقول الهيكل، لا يمكن أن يتكرر اسم الحقل مع اسم الطريقة
الإنشاء
لا توجد طرق بناء في Go، في معظم الحالات يُستخدم الأسلوب التالي لإنشاء الهيكل، عند التهيئة مثل map تُحدد أسماء الحقول ثم تُهيأ قيمها:
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}لكن يمكن أيضاً حذف أسماء الحقول، عند حذف أسماء الحقول، يجب تهيئة جميع الحقول، عادةً لا يُنصح باستخدام هذه الطريقة لأن قابلية القراءة ضعيفة:
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}إذا كانت عملية الإنشاء معقدة، يمكنك كتابة دالة لإنشاء الهيكل، كالتالي، يمكنك أيضاً فهمها كدالة بناء:
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}لكن Go لا تدعم التحميل الزائد للدوال والطرق، لذا لا يمكنك تعريف معاملات مختلفة لنفس الدالة أو الطريقة. إذا أردت إنشاء الهيكل بعدة طرق، إما إنشاء عدة دوال بناء، أو استخدام نمط options.
نمط الخيارات
نمط الخيارات هو نمط تصميم شائع في Go، يمكنه إنشاء الهياكل بمرونة أكبر، مع قابلية توسع عالية، ولا يحتاج لتغيير توقيع دالة البناء. لنفرض الهيكل التالي:
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}تعريف نوع PersonOptions، يقبل معاملاً من نوع *Person، يجب أن يكون مؤشراً لأننا سنريد تعيين قيم لـ Person في الإغلاق:
type PersonOptions func(p *Person)إنشاء دوال الخيارات، عادةً تبدأ بـ With، قيمتها المُرجعة هي دالة إغلاق:
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}توقيع دالة البناء الفعلية كالتالي، تقبل معاملاً متغير الطول من نوع PersonOptions:
func NewPerson(options ...PersonOptions) *Person {
// تطبيق options أولاً
p := &Person{}
for _, option := range options {
option(p)
}
// معالجة القيم الافتراضية
if p.Age < 0 {
p.Age = 0
}
......
return p
}بهذا لاحتياجات إنشاء مختلفة يكفي دالة بناء واحدة، فقط بتمرير دوال Options مختلفة:
func main() {
pl := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}نمط الخيارات الوظيفية يُرى في العديد من المشاريع مفتوحة المصدر، طريقة إنشاء gRPC Server تستخدم أيضاً هذا النمط. نمط الخيارات الوظيفية مناسب فقط للإنشاء المعقد، إذا كانت المعاملات بسيطة وقليلة، يُنصح باستخدام دالة بناء عادية.
التركيب
في Go، العلاقة بين الهياكل تُمثعن طريق التركيب، يمكن التركيب الصريح أو التركيب المجهول، الأخير يشبه الوراثة أكثر في الاستخدام، لكن في الأساس لا يوجد اختلاف. مثلاً:
طريقة التركيب الصريح:
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}عند الاستخدام يجب تحديد الحقل p بشكل صريح:
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)أما التركيب المجهول فلا يحتاج لتحديد الحقل بشكل صريح:
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}اسم الحقل المجهول يكون اسم النوع افتراضياً، يمكن للمستدعي الوصول مباشرة لحقول وطرق هذا النوع، لكن بالإضافة لكونه أكثر راحة، لا يختلف عن الطريقة الأولى:
student := Student{
Person: Person{name: "jack",age: 18},
school: "lili school",
}
fmt.Println(student.name)المؤشرات
بالنسبة لمؤشر الهيكل، لا يحتاج لفك المرجع للوصول لمحتوى الهيكل، مثال:
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)عند الترجمة سيُحول لـ (*p).name و (*p).age، لا يزال فك المرجع مطلوباً، لكن عند الكتابة يمكن حذفه، ويعتبر سكراً نحوياً.
الوسوم
وسام الهيكل هو شكل من أشكال البرمجة الفوقية، مع الانعكاس يمكن صنع وظائف رائعة، الصيغة كالتالي:
`key1:"val1" key2:"val2"`الوسام هو شكل أزواج مفتاح-قيمة، يُفصل بينها بمسافات. تسامح وسام الهيكل مع الأخطاء منخفض جداً، إذا لم يُكتب بالصيغة الصحيحة، لن يمكن قراءته بشكل طبيعي، لكن عند الترجمة لن يكون هناك أي خطأ، فيما يلي مثال للاستخدام:
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}التطبيق الأوسع لوسام الهيكل هو تعريف الأسماء المستعارة في مختلف صيغ التسلسل، استخدام الوسام يحتاج للانعكاس لاستغلال وظائفه بشكل كامل.
محاذاة الذاكرة
توزيع ذاكرة حقول هيكل Go يتبع قواعد محاذاة الذاكرة، هذا يقلل من عدد مرات وصول المعالج للذاكرة، ويزيد من الذاكرة المستخدمة قليلاً، وهي وسيلة لتبديل المساحة بالوقت. لنفرض الهيكل التالي:
type Num struct {
A int64
B int32
C int16
D int8
E int32
}معروف أن هذه الأنواع تشغل بايتات كالتالي:
int64يشغل 8 بايتint32يشغل 4 بايتint16يشغل 2 بايتint8يشغل بايتاً واحداً
هل الذاكرة التي يشغلها الهيكل كله هي 8+4+2+1+4=19 بايت؟ بالطبع لا، وفقاً لقواعد محاذاة الذاكرة، الذاكرة التي يشغلها الهيكل على الأقل ضعف أكبر حقل، والناقص يُكمل. أكبر نوع في هذا الهيكل هو int64 يشغل 8 بايت، إذن توزيع الذاكرة كالتالي:

إذن يشغل فعلياً 24 بايت، منها 5 بايت غير مستخدمة.
لنر الهيكل التالي:
type Num struct {
A int8
B int64
C int8
}بعد فهم القاعدة أعلاه، يمكن فهم أن ذاكرته تشغل أيضاً 24 بايت، رغم أنه يحتوي 3 حقول فقط، هدر 14 بايت:

لكن يمكننا تعديل ترتيب الحقول لتصبح:
type Num struct {
A int8
C int8
B int64
}بهذا تصبح الذاكرة 16 بايت، هدر 6 بايت، توفير 8 بايت:

من الناحية النظرية، جعل حقول الهيكل موزعة بترتيب معقول يمكن تقليل الذاكرة المستخدمة. لكن في عملية البرمجة الفعلية، لا توجد ضرورة لذلك، قد لا يجلب تحسناً ملموساً في تقليل الذاكرة، لكن بالتأكيد سيرفع ضغط دم المبرمجين وعبءهم الذهني، خاصة في بعض الهياكل التي قد تحتوي عشرات أو مئات الحقول، لذا يكفي معرفته فقط.
TIP
إذا كنت تريد فعلاً توفير الذاكرة بهذه الطريقة، يمكنك تجربة هاتين المكتبتين:
ستفحص الكود المصدري للهياكل، وتحسب وتعيد ترتيب حقول الهيكل لتقليل الذاكرة المستخدمة.
الهيكل الفارغ
الهيكل الفارغ ليس له حقول، لا يشغل ذاكرة، يمكننا حساب حجم البايتات عبر دالة unsafe.SizeOf:
func main() {
type Empty struct {}
fmt.Println(unsafe.SizeOf(Empty{}))
}المخرجات:
0هناك العديد من سيناريوهات استخدام الهيكل الفارغ، مثل ما ذكرناه سابقاً، كنوع قيمة لـ map يمكن استخدام map كـ set، أو كنوع للقناة، للإشارة لقناة إشعار فقط.
