1 أساسيات الهيكلية (Struct)
في لغة البرمجة جو (Go)، تُستخدم الهيكلية كنوع بيانات تكويني لتجميع أنواع مختلفة أو متطابقة من البيانات في كيان واحد. تحتل الهيكليات مكانة مهمة في جو حيث تعتبر جزءاً أساسياً من البرمجة الشيئية، على الرغم من وجود اختلافات طفيفة عن لغات البرمجة الشيئية التقليدية.
تنشأ حاجة الهيكليات نتيجة للجوانب التالية:
- تنظيم المتغيرات ذات الصلة القوية معًا لتعزيز قابلية صيانة الكود.
- توفير وسيلة لمحاكاة "الصنف"، مما ييسر ميزات التغليف والتجميع.
- عند التفاعل مع هياكل البيانات مثل JSON، سجلات قواعد البيانات، الخ، تقدم الهيكليات أداة ملائمة لتعيين البيانات.
يسمح تنظيم البيانات باستخدام الهيكليات بتمثيل واضح لنماذج الكائنات في العالم الحقيقي مثل المستخدمين، الطلبات وما إلى ذلك.
2 تعريف الهيكلية
يكون بناء الجملة لتعريف الهيكلية كما يلي:
type اسم_الهيكلية struct {
حقل1 نوع_الحقل1
حقل2 نوع_الحقل2
// ... متغيرات الأعضاء الأخرى
}
- تعريف
type
يقدم تعريف الهيكلية. -
اسم_الهيكلية
هو اسم نوع الهيكلية، ووفقًا لتعليمات جو يتم كتابته عادةً بحروف كبيرة للإشارة إلى إمكانية تصديره. - الكلمة المفتاحية
struct
تدل على أن هذا نوع من الهيكليات. - ضمن الأقواس المعكوسة
{}
، يتم تعريف متغيرات الأعضاء (الحقول) للهيكلية، حيث يتبع كلًا منها نوعها.
يمكن أن يكون نوع أعضاء الهيكلية أي نوع، بما في ذلك الأنواع الأساسية (مثل int
، string
، الخ) والأنواع المعقدة (مثل المصفوفات، الشرائح، هيكلية أخرى، الخ).
على سبيل المثال، تعريف هيكلية تمثل شخص:
type شخص struct {
الاسم string
العمر int
البريد_الإلكتروني []string // يمكن تضمين أنواع معقدة، مثل الشرائح
}
في الكود أعلاه، لدى هيكلية الشخص ثلاث متغيرات من الأعضاء: الاسم
من نوع string، العمر
من نوع integer، والبريد_الإلكتروني
من نوع مصفوفة string، مما يشير إلى إمكانية أن يكون للشخص عدة عناوين بريد إلكتروني.
3 إنشاء وتهيئة الهيكلية
3.1 إنشاء مثيل للهيكلية
هناك طريقتان لإنشاء مثيل للهيكلية: التعريف المباشر أو باستخدام كلمة new
.
التعريف المباشر:
var p شخص
ينشئ الكود أعلاه مثيل p
من نوع شخص
، حيث تكون قيمة كل متغير عضوي في الهيكلية هي القيمة الصفرية لنوعها المقابل.
استخدام كلمة new
:
p := new(شخص)
إنشاء هيكلية باستخدام كلمة new
يؤدي إلى الحصول على مؤشر إلى الهيكلية. المتغير p
في هذه النقطة من نوع *شخص
، والذي يشير إلى متغير مخصص من نوع شخص
حيث تمت تهيئة متغيرات الأعضاء بقيم الصفر.
3.2 تهيئة مثيلات الهيكلية
يمكن تهيئة مثيلات الهيكلية في خطوة واحدة عند إنشائها، باستخدام طريقتين: بأسماء الحقول أو بدون أسماء الحقول.
تهيئة باسماء الحقول:
p := شخص{
الاسم: "أليس",
العمر: 30,
البريد_الإلكتروني: []string{"[email protected]", "[email protected]"},
}
عند التهيئة بشكل تعييني للحقول، لا يحتاج ترتيب التهيئة ليكون مماثلًا لترتيب تعريف الهيكلية، وسيحتفظ أي حقل غير مهيئ بقيمته الصفرية.
تهيئة بدون أسماء الحقول:
p := شخص{"بوب", 25, []string{"[email protected]"}}
عند التهيئة بدون أسماء الحقول، تأكد من أن القيم الابتدائية لكل متغير عضوي هي نفس الترتيب الذي تم تعريف الهيكلية به، ولا يمكن حذف أي حقول.
بالإضافة إلى ذلك، يمكن تهيئة الهيكليات بحقول محددة، حيث ستأخذ أي حقول غير المحددة القيم الصفرية:
p := شخص{الاسم: "تشارلي"}
في هذا المثال، ستحصل فقط الحقل الخاص بالاسم على تهيئة، في حين سيحصل كل من العمر والبريد الإلكتروني على قيمهم الصفرية.
4 الوصول إلى أعضاء الهيكلية
الوصول إلى متغيرات الأعضاء في هيكلية جو يتم بشكل بسيط للغاية، وذلك عن طريق استخدام العامل النقطي (.
). إذا كان لديك متغير هيكلية، يمكنك قراءة أو تعديل قيم أعضائها بهذه الطريقة.
مثال:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// إنشاء متغير من نوع Person
p := Person{"Alice", 30}
// الوصول إلى أعضاء الهيكل
fmt.Println("الإسم:", p.Name)
fmt.Println("العمر:", p.Age)
// تعديل قيم الأعضاء
p.Name = "Bob"
p.Age = 25
// الوصول إلى القيم المعدلة للأعضاء مرة أخرى
fmt.Println("\nالإسم المحدث:", p.Name)
fmt.Println("العمر المحدث:", p.Age)
}
من خلال هذا المثال، نقوم أولاً بتعريف الهيكل Person
بعضوين، Name
و Age
. ثم ننشئ مثالاً من هذا الهيكل ونوضح كيفية قراءة وتعديل هذه الأعضاء.
5 تكوين الهيكل وتضمينه
يمكن للهياكل أن تكون مستقلاً وأيضاً يمكن تضمينها وتضاعفها معاً لإنشاء هياكل بيانات أكثر تعقيداً.
5.1 الهياكل المجهولة
الهيكل المجهول لا يعلن نوعًا جديدًا بشكل صريح، بل يستخدم تعريف الهيكل مباشرة. هذا مفيد عندما تحتاج إلى إنشاء هيكل مرة واحدة واستخدامه ببساطة، مما يتجنب إنشاء أنواع غير ضرورية.
مثال:
package main
import "fmt"
func main() {
// تعريف وتهيئة هيكل مجهول
person := struct {
Name string
Age int
}{
Name: "Eve",
Age: 40,
}
// الوصول إلى أعضاء الهيكل المجهول
fmt.Println("الإسم:", person.Name)
fmt.Println("العمر:", person.Age)
}
في هذا المثال، بدلاً من إنشاء نوع جديد، نحدد هيكلًا مباشرة وننشئ مثالًا منه. يوضح هذا المثال كيفية تهيئة هيكل مجهول والوصول إلى أعضائه.
5.2 تضمين الهياكل
يشمل تضمين الهياكل تضمين هيكل كعضو داخل هيكل آخر. وهذا يسمح لنا ببناء نماذج بيانات أكثر تعقيداً.
مثال:
package main
import "fmt"
// تعريف هيكل Address
type Address struct {
City string
Country string
}
// تضمين هيكل Address كعضو في هيكل Person
type Person struct {
Name string
Age int
Address Address
}
func main() {
// تهيئة مثيل Person
p := Person{
Name: "Charlie",
Age: 28,
Address: Address{
City: "New York",
Country: "USA",
},
}
// الوصول إلى أعضاء الهيكل المضمن
fmt.Println("الإسم:", p.Name)
fmt.Println("العمر:", p.Age)
// الوصول إلى أعضاء هيكل Address
fmt.Println("المدينة:", p.Address.City)
fmt.Println("البلد:", p.Address.Country)
}
في هذا المثال، نعرف هيكل Address
ونضمنه كعضو في هيكل Person
. عند إنشاء مثيل من Person
، نقوم أيضاً بإنشاء مثيل من Address
بشكل متزامن. يمكننا الوصول إلى أعضاء الهيكل المضمن باستخدام التعبير الفارق.
6.2 قيم المُستقبِل والمؤشِّر المُستقبِل
يمكن تصنيف الطرق كمُستقبِلات قيم ومؤشِّرات بناءً على نوع المُستقبِل. تستخدم مُستقبِلات القيم نسخة من الهيكل لاستدعاء الطريق، بينما تستخدم مؤشِّرات المُستقبِل مؤشِّرًا إلى الهيكل ويمكن تعديل الهيكل الأصلي.
// تعريف طريق بمُستقبِل قيم
func (r Rectangle) Perimeter() float64 {
return 2 * (r.length + r.width)
}
// تعريف طريق بمُستقبِل مؤشِّر، الذي يمكن تعديل الهيكل
func (r *Rectangle) SetLength(newLength float64) {
r.length = newLength // يُمكن تعديل قيمة الهيكل الأصلية
}
في المثال أعلاه، Perimeter
هو طريق بمُستقبِل قيم، فاستدعاؤه لن يغيِّر قيمة Rectangle
. ومع ذلك، SetLength
هو طريق بمُستقبِل مؤشِّر، فاستدعاء هذا الطريق سيؤثر على مثيل Rectangle
الأصلي.
6.3 استدعاء الطرق
يمكنك استدعاء طرق هيكل باستخدام المتغير الهيكلي ومؤشِّره.
func main() {
rect := Rectangle{length: 10, width: 5}
// استدعاء الطريق بمُستقبِل قيم
fmt.Println("المساحة:", rect.Area())
// استدعاء الطريق بمُستقبِل قيم
fmt.Println("المحيط:", rect.Perimeter())
// استدعاء الطريق بمُستقبِل مؤشِّر
rect.SetLength(20)
// استدعاء الطريق بمُستقبِل قيم مرة أخرى، لاحظ أن الطول قد تغيّر
fmt.Println("بعد التعديل، المساحة:", rect.Area())
}
عندما تستدعي طريق باستخدام مؤشِّر، يتعامل Go تلقائيًا مع تحويل بين القيم والمؤشِّرات، بغض النظر عما إذا كان طريقك معرفًا بمُستقبِل قيم أو مؤشِّر.
6.4 اختيار نوع المُستقبل
عند تعريف الطرق، يجب عليك أن تقرر ما إذا كنت ستستخدم مُستقبل قيم أم مُستقبل مؤشِّر بناءً على الحالة. إليك بعض الإرشادات الشائعة:
- إذا احتاج الطريق إلى تعديل محتوى الهيكل، استخدم مُستقبل مؤشِّر.
- إذا كان الهيكل كبيرًا وتكلفة النسخ مرتفعة، استخدم مُستقبل مؤشِّر.
- إذا كنت تريد من الطريق تعديل قيمة المُستقبل يشير إليه، استخدم مُستقبل مؤشِّر.
- من الناحية الكفاءة، حتى إذا لم تقم بتعديل محتوى الهيكل، من المعقول استخدام مُستقبل مؤشِّر لهيكل كبير.
- بالنسبة للهياكل الصغيرة، أو عند قراءة البيانات فقط دون الحاجة إلى التعديل، فإن مُستقبل القيم غالبًا ما يكون أبسط وأكثر كفاءة.
من خلال طرق الهيكل، يمكننا محاكاة بعض ميزات البرمجة الموجهة نحو الكائنات في Go، مثل التغليف والطرق. تبسيط هذا النهج في Go يوفر مفهومًا بسيطًا للكائنات مع توفير قدر كافٍ من القدرة على تنظيم وإدارة الوظائف المرتبطة.
7 تسلسل الهياكل وJSON
في Go، من الضروري في كثير من الأحيان تسلسل هيكل إلى شكل JSON للنقل عبر الشبكة أو كملف تكوين. بالمثل، نحتاج أيضًا إلى القدرة على تسلسل JSON إلى مثيلات الهيكل. توفر حزمة encoding/json
في Go هذه الوظيفة.
إليك مثال على كيفية التحويل بين هيكل وJSON:
package main
import (
"encoding/json"
"fmt"
"log"
)
// تعريف الهيكل Person، واستخدام علامات json لتحديد الارتباط بين حقول الهيكل وأسماء حقول JSON
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Emails []string `json:"emails,omitempty"`
}
func main() {
// إنشاء مثيل جديد من Person
p := Person{
Name: "John Doe",
Age: 30,
Emails: []string{"[email protected]", "[email protected]"},
}
// تسلسل إلى JSON
jsonData, err := json.Marshal(p)
if err != nil {
log.Fatalf("فشل تسلسل JSON: %s", err)
}
fmt.Printf("شكل JSON: %s\n", jsonData)
// فك تسلسله إلى هيكل
var p2 Person
if err := json.Unmarshal(jsonData, &p2); err != nil {
log.Fatalf("فشل فك تسلسل JSON: %s", err)
}
fmt.Printf("الهيكل المُسترد: %#v\n", p2)
}
في الكود أعلاه، قمنا بتعريف هيكل Person
، بما في ذلك حقل من نوع قائمة مع الخيار "omitempty". يحدد هذا الخيار أنه إذا كان الحقل فارغًا أو مفقودًا، فلن يُضمن في JSON.
استخدمنا دالة json.Marshal
لتسلسل مثيل هيكل إلى JSON، ودالة json.Unmarshal
لفك تسلسل بيانات JSON إلى مثيل هيكل.
8 مواضيع متقدمة في الهياكل
8.1 مقارنة الهياكل
في لغة Go، يُسمح بمقارنة مباشرة بين حالاتين من الهياكل، ولكن هذه المقارنة تعتمد على قيم الحقول داخل الهياكل. إذا كانت جميع قيم الحقول متساوية، فإن الحالتين من الهياكل تُعتبر متساويتين. يجب ملاحظة أنه لا يمكن مقارنة جميع أنواع الحقول. على سبيل المثال، لا يُمكن مقارنة هيكل يحتوي على شرائح.
فيما يلي مثال لمقارنة الهياكل:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{1, 3}
fmt.Println("p1 == p2:", p1 == p2) // Output: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Output: p1 == p3: false
}
في هذا المثال، يُعتبر p1
وp2
متساويين لأن جميع قيم حقولهما متطابقة. وp3
ليس متساويًا لـp1
لأن قيمة Y
مختلفة.
8.2 نسخ الهياكل
في لغة Go، يمكن نسخ حالات الهياكل باستخدام النسبة. سواء كانت هذه النسخة عميقة أم سطحية يعتمد على أنواع الحقول داخل الهيكل.
إذا احتوى الهيكل على أنواع أساسية فقط (مثل int
، string
، إلخ.)، فإن النسخة ستكون عميقة. إذا احتوى الهيكل على أنواع مرجعية (مثل شرائح، خرائط، إلخ.)، ستكون النسخة سطحية، وسيشترك الحالة الأصلية والحالة المنسوخة في ذاكرة الأنواع المرجعية.
فيما يلي مثال لنسخ هيكل:
package main
import "fmt"
type Data struct {
Numbers []int
}
func main() {
// تهيئة حالة من هيكل Data
original := Data{Numbers: []int{1, 2, 3}}
// نسخ الهيكل
copied := original
// تعديل عناصر الشريحة المُنسوخة
copied.Numbers[0] = 100
// عرض عناصر الحالة الأصلية والمُنسوخة
fmt.Println("Original:", original.Numbers) // Output: Original: [100 2 3]
fmt.Println("Copied:", copied.Numbers) // Output: Copied: [100 2 3]
}
كما هو موضح في المثال، تشترك الحالة الأصلية والمُنسوخة في نفس الشريحة، لذا فإن تعديل بيانات الشريحة في copied
سيؤثر أيضًا على بيانات الشريحة في original
.
لتجنب هذه المشكلة، يمكنك تحقيق النسخ العميق الحقيقي عن طريق نسخ محتويات الشريحة إلى شريحة جديدة:
newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}
بهذه الطريقة، لن يؤثر أي تعديلات على copied
على original
.