Skip to content

هياكل Go

الهيكل يمكنه تخزين مجموعة من البيانات بأنواع مختلفة، وهو نوع مركب. تخلت Go عن الصفوف والوراثة، كما تخلت عن طرق البناء، وأضعفت عمداً وظائف البرمجة كائنية التوجه، Go ليست لغة OOP تقليدية، لكن لا تزال هناك ظل لـ OOP في Go، من خلال الهياكل والطرق يمكن محاكاة فصل. فيما يلي مثال بسيط للهيكل:

go
type Programmer struct {
  Name     string
  Age      int
  Job      string
  Language []string
}

التعريف

تعريف الهيكل بسيط جداً، مثال:

go
type Person struct {
   name string
   age int
}

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

go
type Rectangle struct {
  height, width, area int
  color               string
}

TIP

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

الإنشاء

لا توجد طرق بناء في Go، في معظم الحالات يُستخدم الأسلوب التالي لإنشاء الهيكل، عند التهيئة مثل map تُحدد أسماء الحقول ثم تُهيأ قيمها:

go
programmer := Programmer{
   Name:     "jack",
   Age:      19,
   Job:      "coder",
   Language: []string{"Go", "C++"},
}

لكن يمكن أيضاً حذف أسماء الحقول، عند حذف أسماء الحقول، يجب تهيئة جميع الحقول، عادةً لا يُنصح باستخدام هذه الطريقة لأن قابلية القراءة ضعيفة:

go
programmer := Programmer{
   "jack",
   19,
   "coder",
   []string{"Go", "C++"}}

إذا كانت عملية الإنشاء معقدة، يمكنك كتابة دالة لإنشاء الهيكل، كالتالي، يمكنك أيضاً فهمها كدالة بناء:

go
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، يمكنه إنشاء الهياكل بمرونة أكبر، مع قابلية توسع عالية، ولا يحتاج لتغيير توقيع دالة البناء. لنفرض الهيكل التالي:

go
type Person struct {
  Name     string
  Age      int
  Address  string
  Salary   float64
  Birthday string
}

تعريف نوع PersonOptions، يقبل معاملاً من نوع *Person، يجب أن يكون مؤشراً لأننا سنريد تعيين قيم لـ Person في الإغلاق:

go
type PersonOptions func(p *Person)

إنشاء دوال الخيارات، عادةً تبدأ بـ With، قيمتها المُرجعة هي دالة إغلاق:

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

go
func NewPerson(options ...PersonOptions) *Person {
    // تطبيق options أولاً
  p := &Person{}
    for _, option := range options {
        option(p)
    }

  // معالجة القيم الافتراضية
  if p.Age < 0 {
    p.Age = 0
  }
  ......

    return p
}

بهذا لاحتياجات إنشاء مختلفة يكفي دالة بناء واحدة، فقط بتمرير دوال Options مختلفة:

go
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، العلاقة بين الهياكل تُمثعن طريق التركيب، يمكن التركيب الصريح أو التركيب المجهول، الأخير يشبه الوراثة أكثر في الاستخدام، لكن في الأساس لا يوجد اختلاف. مثلاً:

طريقة التركيب الصريح:

go
type Person struct {
   name string
   age  int
}

type Student struct {
   p      Person
   school string
}

type Employee struct {
   p   Person
   job string
}

عند الاستخدام يجب تحديد الحقل p بشكل صريح:

go
student := Student{
   p:      Person{name: "jack", age: 18},
   school: "lili school",
}
fmt.Println(student.p.name)

أما التركيب المجهول فلا يحتاج لتحديد الحقل بشكل صريح:

go
type Person struct {
  name string
  age  int
}

type Student struct {
  Person
  school string
}

type Employee struct {
  Person
  job string
}

اسم الحقل المجهول يكون اسم النوع افتراضياً، يمكن للمستدعي الوصول مباشرة لحقول وطرق هذا النوع، لكن بالإضافة لكونه أكثر راحة، لا يختلف عن الطريقة الأولى:

go
student := Student{
   Person: Person{name: "jack",age: 18},
   school: "lili school",
}
fmt.Println(student.name)

المؤشرات

بالنسبة لمؤشر الهيكل، لا يحتاج لفك المرجع للوصول لمحتوى الهيكل، مثال:

go
p := &Person{
   name: "jack",
   age:  18,
}
fmt.Println(p.age,p.name)

عند الترجمة سيُحول لـ (*p).name و (*p).age، لا يزال فك المرجع مطلوباً، لكن عند الكتابة يمكن حذفه، ويعتبر سكراً نحوياً.

الوسوم

وسام الهيكل هو شكل من أشكال البرمجة الفوقية، مع الانعكاس يمكن صنع وظائف رائعة، الصيغة كالتالي:

go
`key1:"val1" key2:"val2"`

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

go
type Programmer struct {
    Name     string `json:"name"`
    Age      int `yaml:"age"`
    Job      string `toml:"job"`
    Language []string `properties:"language"`
}

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

محاذاة الذاكرة

توزيع ذاكرة حقول هيكل Go يتبع قواعد محاذاة الذاكرة، هذا يقلل من عدد مرات وصول المعالج للذاكرة، ويزيد من الذاكرة المستخدمة قليلاً، وهي وسيلة لتبديل المساحة بالوقت. لنفرض الهيكل التالي:

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 بايت غير مستخدمة.

لنر الهيكل التالي:

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

go
func main() {
   type Empty struct {}
   fmt.Println(unsafe.SizeOf(Empty{}))
}

المخرجات:

0

هناك العديد من سيناريوهات استخدام الهيكل الفارغ، مثل ما ذكرناه سابقاً، كنوع قيمة لـ map يمكن استخدام map كـ set، أو كنوع للقناة، للإشارة لقناة إشعار فقط.

Golang تم تحريره بواسطة www.golangdev.cn