التراكيب
التراكيب
تخيل أنك تملأ استمارة معلومات شخصية: الاسم، العمر، رقم الهاتف، العنوان — هذه المعلومات مترابطة ببعضها وتصف شخصًا معًا. في Go، الـ struct هو تلك "الاستمارة": يجمع عدة حقول من أنواع مختلفة في نوع مخصص مركب. مقارنة بالخرائط، يتم تحديد حقول التراكيب وقت الترجمة مما يجعلها آمنة نوعيًا وواضحًا دلاليًا. التراكيب هي الطريقة الأساسية لتنظيم البيانات في Go.
1. المفاهيم الأساسية
| المفهوم | الوصف |
|---|---|
| التعريف | type StructName struct { ... }، اسم الحقل يأتي قبل النوع |
| التهيئة | حرفي S{f1: v1, f2: v2}، new(S) يُرجع مؤشرًا، &S{} |
| الوصول إلى الحقول | كتابة النقطة s.Field، المؤشرات تستخدم أيضًا p.Field (لا حاجة لـ ->) |
| نوع القيمة | التراكيب أنواع قيم؛ التعيين ووسيطات الدوال تنسخ التراكيب بالكامل |
| مستلم المؤشر | احصل على مؤشر عبر &s؛ تعديل التراكيب عبر المؤشر يؤثر على القيمة الأصلية |
| الحقول المجهولة | اكتب النوع فقط بدون اسم حقل؛ يصبح اسم النوع هو اسم الحقل مما يتيح "التركيب" |
| التراكيب المتداخلة | يمكن لحقول التراكيب أن تكون تراكيب بنفسها، وتدعم التداخل متعدد المستويات |
| علامات التراكيب | بيانات وصفية محاطة بعلامات اقتباس عكسية، تُستخدم عادةً لتعيين JSON/قاعدة البيانات |
*S.
2. الصيغة الأساسية/الاستخدام
تعريف تراكيب
// تعريف تراكيب Person
type Person struct {
Name string
Age int
City string
}
طرق التهيئة
// الطريقة 1: اسم الحقل: القيمة (موصى بها، سهولة القراءة، الترتيب لا يهم)
p1 := Person{Name: "Alice", Age: 25, City: "Beijing"}
// الطريقة 2: التعيين حسب الترتيب (غير موصى بها للعديد من الحقول، عرضة للأخطاء)
p2 := Person{"Bob", 30, "Shanghai"}
// الطريقة 3: الإعلان أولاً ثم التعيين
var p3 Person
p3.Name = "Charlie"
p3.Age = 28
p3.City = "Guangzhou"
// الطريقة 4: الإنشاء باستخدام new (يُرجع مؤشرًا)
p4 := new(Person) // *Person
p4.Name = "Diana"
// الطريقة 5: أخذ العنوان (يُرجع مؤشرًا)
p5 := &Person{Name: "Eve", Age: 22, City: "Shenzhen"}
var p Person تُهيّئ جميع الحقول إلى القيمة الصفرية لأنواعها (سلسلة ← ""، عدد صحيح ← 0، منطقي ← false).
الوصول إلى الحقول وتعديلها
p := Person{Name: "Alice", Age: 25}
// قراءة حقل
fmt.Println(p.Name) // Alice
// تعديل حقل
p.Age = 26
fmt.Println(p.Age) // 26
p->Name.
مؤشرات التراكيب
p := &Person{Name: "Alice", Age: 25}
// Go يحلل المؤشر تلقائيًا، استخدم كتابة النقطة مباشرة
fmt.Println(p.Name) // Alice
// تعديل القيمة الأصلية عبر المؤشر
p.Age = 30
->. سواء كان p قيمة أو مؤشرًا، تستخدم دائمًا p.Field للوصول إلى الحقول.
3. أمثلة الكود
مثال 1: الاستخدام الأساسي (الصعوبة ⭐)
تعريف تراكيب Book وإنشاء كائنات وطباعة المعلومات.
package main
import "fmt"
// Book يُعرّف تراكيب كتاب
type Book struct {
Title string
Author string
Pages int
Price float64
}
func main() {
// الإنشاء باستخدام الكتابة الحرفية
book1 := Book{
Title: "The Go Programming Language",
Author: "Alan Donovan",
Pages: 350,
Price: 59.9,
}
// طباعة التراكيب
fmt.Println(book1)
// الوصول إلى حقول مفردة
fmt.Printf("Title: %s\n", book1.Title)
fmt.Printf("Author: %s\n", book1.Author)
fmt.Printf("Price: %.1f\n", book1.Price)
// تعديل حقل
book1.Price = 49.9
fmt.Printf("New price: %.1f\n", book1.Price)
// الإنشاء حسب الترتيب (غير موصى به)
book2 := Book{"Python Crash Course", "Eric Matthes", 280, 45.0}
fmt.Println(book2)
}
الناتج:
{The Go Programming Language Alan Donovan 350 59.9}
Title: The Go Programming Language
Author: Alan Donovan
Price: 59.9
New price: 49.9
{Python Crash Course Eric Matthes 280 45}
مثال 2: الاستخدام المتقدم (الصعوبة ⭐⭐)
استخدام المؤشرات والتراكيب المتداخلة والحقول المجهولة.
package main
import "fmt"
// تراكيب العنوان
type Address struct {
City string
Street string
ZipCode string
}
// تراكيب الموظف (يستخدم الحقل المجهول Address)
type Employee struct {
Name string
Age int
Address // حقل مجهول: اسم النوع هو اسم الحقل
Salary float64
}
func main() {
// ========== تهيئة التراكيب المتداخلة ==========
emp := Employee{
Name: "Alice",
Age: 30,
Salary: 15000,
Address: Address{
City: "Beijing",
Street: "1 Zhongguancun Street",
ZipCode: "100080",
},
}
fmt.Printf("Employee: %s\n", emp.Name)
fmt.Printf("City: %s\n", emp.Address.City) // وصول صريح
// ========== الوصول "الترقية" للحقول المجهولة ==========
// يمكن الوصول إلى الحقول المجهولة مباشرة باسم النوع
fmt.Printf("Street: %s\n", emp.Street) // نفس emp.Address.Street
fmt.Printf("ZipCode: %s\n", emp.ZipCode) // نفس emp.Address.ZipCode
// ========== عمليات المؤشرات ==========
empPtr := &emp
// التعديل عبر المؤشر (Go يحلل المؤشر تلقائيًا)
empPtr.Age = 31
empPtr.Salary = 18000
empPtr.City = "Shanghai" // تعديل الحقل المجهول عبر المؤشر
fmt.Printf("\nAfter modification: %+v\n", *empPtr)
// ========== نوع القيمة مقابل نوع المؤشر ==========
fmt.Println("\n--- Value type vs Pointer type ---")
// نوع القيمة: التعيين هو النسخ
emp2 := emp
emp2.Name = "Bob"
fmt.Printf("emp.Name: %s\n", emp.Name) // لا يزال Alice
fmt.Printf("emp2.Name: %s\n", emp2.Name) // Bob
// نوع المؤشر: التعيين هو المشاركة
emp3 := empPtr
emp3.Name = "Charlie"
fmt.Printf("empPtr.Name: %s\n", empPtr.Name) // Charlie (تم التعديل)
}
الناتج:
Employee: Alice
City: Beijing
Street: 1 Zhongguancun Street
ZipCode: 100080
After modification: {Name:Alice Age:31 Address:{City:Shanghai Street:1 Zhongguancun Street ZipCode:100080} Salary:18000}
--- Value type vs Pointer type ---
emp.Name: Alice
emp2.Name: Bob
empPtr.Name: Charlie
مثال 3: التطبيق الشامل (الصعوبة ⭐⭐⭐)
استخدام علامات التراكيب لسلسلة/استرجاع JSON، وبناء نظام إدارة طلاب.
package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// Student تراكيب الطالب مع علامات JSON
type Student struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Score float64 `json:"score"`
Tags []string `json:"tags,omitempty"` // omitempty: يُحذف عندما يكون فارغًا
secret string // حقل غير مُصدَّر، غير قابل للوصول عبر JSON
}
// ClassRoom (تراكيب متداخل)
type ClassRoom struct {
ClassName string `json:"class_name"`
Students []Student `json:"students"`
}
// AddStudent يضيف طالبًا
func (c *ClassRoom) AddStudent(s Student) {
c.Students = append(c.Students, s)
}
// GetTopN يحصل على أفضل N طلاب حسب الدرجة
func (c *ClassRoom) GetTopN(n int) []Student {
// عمل نسخة لتجنب تعديل البيانات الأصلية
sorted := make([]Student, len(c.Students))
copy(sorted, c.Students)
// الترتيب حسب الدرجة تنازليًا
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Score > sorted[j].Score
})
if n > len(sorted) {
n = len(sorted)
}
return sorted[:n]
}
// Average يحسب متوسط الدرجات
func (c *ClassRoom) Average() float64 {
if len(c.Students) == 0 {
return 0
}
total := 0.0
for _, s := range c.Students {
total += s.Score
}
return total / float64(len(c.Students))
}
// SearchByName يبحث بالاسم (مطابقة ضبابية)
func (c *ClassRoom) SearchByName(keyword string) []Student {
var result []Student
for _, s := range c.Students {
if strings.Contains(s.Name, keyword) {
result = append(result, s)
}
}
return result
}
func main() {
// إنشاء فصل دراسي
class := &ClassRoom{ClassName: "Class 1, Grade 2024"}
// إضافة طلاب
students := []Student{
{ID: 1, Name: "Zhang San", Age: 18, Score: 92.5, Tags: []string{"good at math", "class monitor"}},
{ID: 2, Name: "Li Si", Age: 19, Score: 88.0, Tags: []string{"good at sports"}},
{ID: 3, Name: "Wang Wu", Age: 18, Score: 95.5},
{ID: 4, Name: "Zhao Liu", Age: 20, Score: 78.0, Tags: []string{"art talent"}},
{ID: 5, Name: "Zhang Wei", Age: 19, Score: 91.0, Tags: []string{"good at math", "competition winner"}},
}
for _, s := range students {
class.AddStudent(s)
}
// ========== سلسلة JSON ==========
fmt.Println("=== JSON Serialization ===")
jsonData, err := json.MarshalIndent(class, "", " ")
if err != nil {
fmt.Println("Serialization failed:", err)
} else {
fmt.Println(string(jsonData))
}
// ========== استرجاع JSON ==========
fmt.Println("\n=== JSON Deserialization ===")
jsonStr := `{"id":6,"name":"Sun Qi","age":17,"score":97.0,"tags":["good at English"]}`
var newStudent Student
if err := json.Unmarshal([]byte(jsonStr), &newStudent); err != nil {
fmt.Println("Deserialization failed:", err)
} else {
fmt.Printf("New student: %+v\n", newStudent)
}
// ========== عرض الميزات ==========
fmt.Println("\n=== Class Info ===")
fmt.Printf("Class: %s, total %d students\n", class.ClassName, len(class.Students))
fmt.Printf("Average score: %.1f\n", class.Average())
fmt.Println("\n=== Top 3 Scores ===")
top3 := class.GetTopN(3)
for i, s := range top3 {
fmt.Printf(" #%d: %s (%.1f)\n", i+1, s.Name, s.Score)
}
fmt.Println("\n=== Search 'Zhang' ===")
results := class.SearchByName("Zhang")
for _, s := range results {
fmt.Printf(" %s (ID: %d, Score: %.1f)\n", s.Name, s.ID, s.Score)
}
}
الناتج:
=== JSON Serialization ===
{
"class_name": "Class 1, Grade 2024",
"students": [
{
"id": 1,
"name": "Zhang San",
"age": 18,
"score": 92.5,
"tags": [
"good at math",
"class monitor"
]
},
{
"id": 2,
"name": "Li Si",
"age": 19,
"score": 88
},
{
"id": 3,
"name": "Wang Wu",
"age": 18,
"score": 95.5
},
{
"id": 4,
"name": "Zhao Liu",
"age": 20,
"score": 78,
"tags": [
"art talent"
]
},
{
"id": 5,
"name": "Zhang Wei",
"age": 19,
"score": 91,
"tags": [
"good at math",
"competition winner"
]
}
]
}
=== JSON Deserialization ===
New student: {ID:6 Name:Sun Qi Age:17 Score:97 Tags:[good at English]}
=== Class Info ===
Class: Class 1, Grade 2024, total 5 students
Average score: 89.0
=== Top 3 Scores ===
#1: Wang Wu (95.5)
#2: Zhang San (92.5)
#3: Zhang Wei (91.0)
=== Search 'Zhang' ===
Zhang San (ID: 1, Score: 92.5)
Zhang Wei (ID: 5, Score: 91.0)
3. سيناريوهات التطبيق الشائعة
السيناريو 1: تفاعل بيانات JSON
في تطوير الويب، التراكيب هي الطريقة المعيارية للتعامل مع طلبات/استجابات JSON. العلامات التحكم في أسماء حقول JSON.
package main
import (
"encoding/json"
"fmt"
)
// APIResponse هيكل استجابة API موحد
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// UserDTO كائن نقل بيانات المستخدم
type UserDTO struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"` // "-" يعني تجاهل هذا الحقل في JSON
}
func main() {
// بناء استجابة
user := UserDTO{
ID: 1,
Username: "alice",
Email: "alice@example.com",
Password: "secret123", // لن يُسلسل
}
resp := APIResponse{
Code: 200,
Message: "success",
Data: user,
}
// السلسلة
jsonBytes, _ := json.MarshalIndent(resp, "", " ")
fmt.Println(string(jsonBytes))
// الاسترجاع
jsonStr := `{"code":404,"message":"User not found"}`
var errResp APIResponse
json.Unmarshal([]byte(jsonStr), &errResp)
fmt.Printf("\nError code: %d, Message: %s\n", errResp.Code, errResp.Message)
}
الناتج:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"username": "alice",
"email": "alice@example.com"
}
}
Error code: 404, Message: User not found
السيناريو 2: محاكاة الكائنية (التركيب بدل الوراثة)
ليس في Go فئات أو وراثة؛ يتم تحقيق إعادة استخدام الكود عبر تداخل التراكيب (التركيب).
package main
import "fmt"
// Logger قدرة التسجيل
type Logger struct {
Prefix string
}
// Log يُخرج رسالة سجل
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.Prefix, msg)
}
// Cache قدرة التخزين المؤقت
type Cache struct {
data map[string]string
}
// Get يسترجع من التخزين المؤقت
func (c *Cache) Get(key string) (string, bool) {
val, ok := c.data[key]
return val, ok
}
// Set يُعيّن إدخال تخزين مؤقت
func (c *Cache) Set(key, value string) {
if c.data == nil {
c.data = make(map[string]string)
}
c.data[key] = value
}
// UserService يعيد استخدام قدرتي Logger و Cache عبر التركيب
type UserService struct {
Logger // تضمين Logger
Cache // تضمين Cache
Name string
}
func main() {
svc := UserService{
Logger: Logger{Prefix: "UserService"},
Name: "User Service",
}
// استدعاء الأساليب المضمنة مباشرة
svc.Log("Service started") // من Logger
svc.Set("user:1", "Alice") // من Cache
svc.Log("Cache initialized")
if val, ok := svc.Get("user:1"); ok {
svc.Log(fmt.Sprintf("Found user: %s", val))
}
}
الناتج:
[UserService] Service started
[UserService] Cache initialized
[UserService] Found user: Alice
❓ أسئلة شائعة
س1: هل التراكيب نوع قيمة أم نوع مرجع؟
التراكيب أنواع قيم. التعيين ووسيطات الدوال تنسخ التراكيب بالكامل. لتعديل القيمة الأصلية، مرر مؤشرًا:
func updateAge(p *Person, age int) {
p.Age = age // يُعدّل القيمة الأصلية مباشرة
}
p := Person{Name: "Alice", Age: 25}
updateAge(&p, 30)
fmt.Println(p.Age) // 30
س2: ما فائدة الحقول المجهولة؟ وكيف تختلف عن التراكيب المتداخلة؟
الحقول المجهولة تُحقق "التركيب". أساليب وحقول النوع الداخلي "تُرقى" إلى النوع الخارجي:
type Address struct {
City string
}
type Person struct {
Name string
Address // حقل مجهول
}
p := Person{Name: "Alice", Address: Address{City: "Beijing"}}
fmt.Println(p.City) // وصول مباشر، نفس p.Address.City
fmt.Println(p.Address.City) // يعمل أيضًا بالوصول الصريح
إذا كان للتراكيب المتداخلة حقول بنفس الاسم، تحتاج لاستخدام المسار الكامل للتمييز.
س3: ما هي علامات التراكيب؟ كيف أستخدمها؟
العلامات هي بيانات وصفية محاطة بعلامات اقتباس عكسية، تُستخدم عادةً للتحكم في سلوك JSON و ORM قواعد البيانات والمكتبات الأخرى:
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" db:"user_name"`
Password string `json:"-" db:"password"` // يُتجاهل في JSON
Email string `json:"email,omitempty" db:"email"` // يُحذف في JSON عندما يكون فارغًا
}
العلامات الشائعة:
json:"fieldName"— يتحكم في اسم حقل JSONjson:"-"— يتجاهل الحقل أثناء السلسلةjson:",omitempty"— يُحذف عند القيمة الصفريةdb:"column_name"— تعيين حقل قاعدة البيانات (يتطلب دعم ORM)
س4: هل يمكن مقارنة التراكيب؟
يعتمد على أنواع الحقول. إذا كانت جميع الحقول أنواع قابلة للمقارنة (int، string، bool، مصفوفات، إلخ)، يمكن مقارنة التراكيب بـ == و !=. إذا كانت تحتوي على حقول غير قابلة للمقارنة مثل الشرائح أو الخرائط أو الدوال، لا يمكن مقارنتها مباشرة:
type Point struct { X, Y int }
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true
// التراكيب التي تحتوي شرائح لا يمكن استخدام ==
type Data struct { Items []int }
// d1 == d2 // خطأ ترجمة!
📖 ملخص
- struct هو النوع المخصص المركب في Go، يجمع عدة حقول في وحدة واحدة
- التراكيب أنواع قيم؛ التعيين ووسيطات الدوال تنسخ التراكيب بالكامل؛ مرر مؤشرًا
*Sلتعديل الأصل - الوصول إلى الحقول يستخدم كتابة النقطة
s.Field؛ المؤشرات تستخدم أيضًاp.Field، لا حاجة لـ-> - الحقول المجهولة (نوع فقط، بدون اسم حقل) تُحقق التركيب؛ الحقول الداخلية "تُرقى" إلى المستوى الخارجي
- التراكيب المتداخلة تدعم تنظيم البيانات متعدد المستويات
- علامات التراكيب تُعرّف بعلامات الاقتباس العكسية، تُستخدم عادةً لتعيين حقول JSON/ORM
json:"-"يتجاهل حقلًا؛json:",omitempty"يُحذف القيم الصفرية- قابلية مقارنة التراكيب تعتمد على أنواع حقولها
📝 تمارين
تمرين 1 (⭐)
عرّف تراكيب Rectangle بحقولي Width و Height. نفّذ ما يلي:
- أنشئ كائن Rectangle واطبعه
- اكتب دالة
Area()لحساب المساحة - اكتب دالة
Perimeter()لحساب المحيط - اكتب دالة
IsSquare()للتحقق مما إذا كان مربعًا
تمرين 2 (⭐⭐)
عرّف تراكيب BankAccount بحقلي Owner (سلسلة) و Float64 (float64). نفّذ ما يلي:
Deposit(amount)للإيداعWithdraw(amount)للسحب (أبلغ عند عدم كفاية الرصيد)Transfer(other *BankAccount, amount)للتحويل- استخدم علامات JSON لسلسلة معلومات الحساب (أخفِ الرصيد باستخدام
json:"-")
تمرين 3 (⭐⭐⭐)
نفّذ نظام إدارة مخزون المنتجات:
- عرّف تراكيب
Product(المعرّف، الاسم، السعر، المخزون، العلامات) - عرّف تراكيب
Inventory(اسم المتجر، شريحة المنتجات) - نفّذ الأساليب:
AddProduct،RemoveProduct(id)،FindByTag(tag)،TotalValue()(إجمالي قيمة المخزون) - نفّذ سلسلة JSON مع المتطلبات: السعر إلى خانتين عشريتين، حذف المخزون عند 0
- أنشئ كائنات، أضف منتجات، ابحث بالعلامة، احسب القيمة الكلية، وأخرج كـ JSON



