المصفوفات و Slices

المصفوفات و Slices

تخيّل أنك تشتري صندوق حليب كامل في السوبرماركت — المصفوفة مثل ذلك الصندوق بسعة ثابتة لا يمكن أن يحتمل أكثر عندما يمتلئ؛ و slice مثل كيس التسوق الذي يمكن أن يتوسع — عندما يكون لديك المزيد من الأشياء، فقط انتقل إلى كيس أكبر. في Go، المصفوفات لها طول ثابت وأنواع قيم؛ و slices لها طول متغير وأنواع مراجع. عملياً، slices تُستخدم بكثرة أكثر من المصفوفات، لكن فهم المصفوفات هو الأساس لفهم slices.


1. المفاهيم الأساسية

المفهوم الوصف
مصفوفة [N]T طول ثابت؛ يجب تحديد الحجم عند الإعلان؛ نوع قيمة — الإسناد وتمرير المعاملات ينسخ المصفوفة بالكامل
slice []T طول متغير؛ تشير إلى مصفوفة أساسية؛ نوع مرجع — الإسناد وتمرير المعاملات يتشاركان المصفوفة الأساسية
make([]T, len, cap) الطريقة المُوصى بها لإنشاء slices؛ يمكن تحديد الطول والسعة
append(slice, elems...) يُضيف عناصر إلى slice، يُعيد slice جديدة؛ قد يُطلق توسيع المصفوفة الأساسية
copy(dst, src) ينسخ المحتويات من slice src إلى dst، يُعيد عدد العناصر المُنسوخة
الطول len() العدد الفعلي للعناصر في slice
السعة cap() عدد العناصر من بداية slice إلى نهاية المصفوفة الأساسية

2. الصياغة والاستخدام الأساسي

إعلان وتهيئة المصفوفات

GO
// إعلان مصفوفة int بطول 5، تهيئة بقيمة صفرية
var arr1 [5]int

// إعلان وتهيئة
var arr2 = [5]int{1, 2, 3, 4, 5}

// السماح للمترجم باستنتاج الطول
arr3 := [...]int{10, 20, 30}

// تهيئة بفهارس محددة
arr4 := [5]int{1: 100, 3: 300} // [0, 100, 0, 300, 0]

إعلان وتهيئة Slices

GO
// إعلان slice فارغة (القيمة الصفرية nil، لكن يمكن append مباشرة)
var s1 []int

// تهيئة حرفية
s2 := []int{1, 2, 3}

// إنشاء slice من مصفوفة
arr := [5]int{10, 20, 30, 40, 50}
s3 := arr[1:4] // [20, 30, 40]

// إنشاء slice بـ make
s4 := make([]int, 5)      // طول 5، سعة 5
s5 := make([]int, 3, 10)  // طول 3، سعة 10
💡 نصيحة: slices الفارغة nil و slices الفارغة ([]int{} أو make([]int, 0)) متكافئة وظيفياً — len و cap كلاهما 0، و append يعمل بشكل طبيعي. الفرق هو أن slices الفارغة nil تُسلسل إلى null في JSON، بينما slices الفارغة تُسلسل إلى [].

عمليات Slice

GO
s := []int{10, 20, 30, 40, 50}

// قطع slice فرعية [مغلق من اليسار، مفتوح من اليمين)
s1 := s[1:3]   // [20, 30]
s2 := s[:3]    // [10, 20, 30]، من البداية
s3 := s[2:]    // [30, 40, 50]، إلى النهاية
s4 := s[:]     // [10, 20, 30, 40, 50]، slice كاملة

// الطول والسعة
fmt.Println(len(s))  // 5
fmt.Println(cap(s))  // 5

// إضافة عناصر
s = append(s, 60)            // إضافة عنصر واحد
s = append(s, 70, 80, 90)    // إضافة عدة عناصر
s = append(s, []int{100}...) // إضافة slice أخرى

// نسخ slices
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // dst = [1, 2, 3]، n = 3
💡 نصيحة: append يُعيد slice جديدة؛ يجب التقاط قيمة الإرجاع. عندما len(s) == cap(s)، يُخصص append مصفوفة أساسية جديدة، و slices القديمة والجديدة لا تتشاركان البيانات بعد الآن.

💡 نصيحة: عند استخدام s[low:high] للقطع، تتشارك slice الجديدة المصفوفة الأساسية مع الأصل. تعديل العناصر في slice الفرعية يؤثر على الأصل. استخدم copy إذا كنت بحاجة إلى نسخة مستقلة.


3. أمثلة الكود

مثال 1: الاستخدام الأساسي (الصعوبة ⭐)

GO
package main

import "fmt"

func main() {
	// ========== المصفوفات ==========
	// إعلان وتهيئة مصفوفة من 5 درجات
	scores := [5]int{90, 85, 78, 92, 88}
	fmt.Println("مصفوفة الدرجات:", scores)

	// الوصول إلى العناصر وتعديلها بالفهرس
	fmt.Println("الدرجة الأولى:", scores[0])
	scores[2] = 80 // تغيير الدرجة الثالثة إلى 80
	fmt.Println("بعد التعديل:", scores)

	// iterate على المصفوفة: حلقة for تقليدية
	fmt.Println("\n--- iterate بـ for تقليدي ---")
	for i := 0; i < len(scores); i++ {
		fmt.Printf("الدرجة %d: %d\n", i+1, scores[i])
	}

	// iterate على المصفوفة: range
	fmt.Println("\n--- iterate بـ range ---")
	for index, value := range scores {
		fmt.Printf("الفهرس %d: %d\n", index, value)
	}

	// ========== Slices ==========
	// اشتقاق slice من مصفوفة
	top3 := scores[0:3] // أول 3 درجات
	fmt.Println("\nأول 3 درجات:", top3)

	// إنشاء slice بحرفية
	fruits := []string{"تفاح", "موز", "برتقال"}
	fmt.Println("slice الفواكه:", fruits)

	// إضافة عنصر بـ append
	fruits = append(fruits, "عنب")
	fmt.Println("بعد إضافة العنب:", fruits)

	// الطول والسعة
	fmt.Printf("الطول: %d، السعة: %d\n", len(fruits), cap(fruits))
}
▶ جرّب الكود

المخرجات:

TEXT
مصفوفة الدرجات: [90 85 78 92 88]
الدرجة الأولى: 90
بعد التعديل: [90 85 80 92 88]

--- iterate بـ for تقليدي ---
الدرجة 1: 90
الدرجة 2: 85
الدرجة 3: 80
الدرجة 4: 92
الدرجة 5: 88

--- iterate بـ range ---
الفهرس 0: 90
الفهرس 1: 85
الفهرس 2: 80
الفهرس 3: 92
الفهرس 4: 88

أول 3 درجات: [90 85 80]
slice الفواكه: [تفاح موز برتقال]
بعد إضافة العنب: [تفاح موز برتقال عنب]
الطول: 4، السعة: 4

مثال 2: الاستخدام المتوسط (الصعوبة ⭐⭐)

GO
package main

import "fmt"

func main() {
	// ========== append والتوسع ==========
	// إنشاء slice بسعة 3
	s := make([]int, 0, 3)
	fmt.Printf("الأولية: len=%d, cap=%d, %v\n", len(s), cap(s), s)

	// إضافة واحداً تلو الآخر، مراقبة تغيرات السعة
	for i := 1; i <= 5; i++ {
		s = append(s, i)
		fmt.Printf("بعد إضافة %d: len=%d, cap=%d, %v\n", i, len(s), cap(s), s)
	}

	// ========== slices تتشارك المصفوفة الأساسية ==========
	original := []int{10, 20, 30, 40, 50}
	sub := original[1:3] // [20, 30]

	fmt.Println("\n--- عرض المصفوفة الأساسية المشتركة ---")
	fmt.Println("slice الأصلية:", original)
	fmt.Println("slice الفرعية:", sub)

	// تعديل slice الفرعية يؤثر على الأصلية
	sub[0] = 999
	fmt.Println("\nبعد تعديل slice الفرعية:")
	fmt.Println("slice الأصلية:", original) // original[1] تغيرت أيضاً
	fmt.Println("slice الفرعية:", sub)

	// ========== استخدام copy لإنشاء نسخة مستقلة ==========
	fmt.Println("\n--- استخدام copy لنسخة مستقلة ---")
	original = []int{10, 20, 30, 40, 50}
	// قطع أولاً، ثم نسخ بشكل مستقل
	subSlice := original[1:4] // [20, 30, 40]
	independent := make([]int, len(subSlice))
	copy(independent, subSlice)

	independent[0] = 888
	fmt.Println("slice الأصلية:", original)       // لم تتأثر
	fmt.Println("النسخة المستقلة:", independent)   // النسخة فقط تغيرت

	// ========== دمج sliceتين ==========
	fmt.Println("\n--- دمج slices ---")
	a := []int{1, 2, 3}
	b := []int{4, 5, 6}
	merged := append(a, b...)
	fmt.Println("نتيجة الدمج:", merged)
}
▶ جرّب الكود

المخرجات:

TEXT
الأولية: len=0, cap=3, []
بعد إضافة 1: len=1, cap=3, [1]
بعد إضافة 2: len=2, cap=3, [1 2]
بعد إضافة 3: len=3, cap=3, [1 2 3]
بعد إضافة 4: len=4, cap=6, [1 2 3 4]
بعد إضافة 5: len=5, cap=6, [1 2 3 4 5]

--- عرض المصفوفة الأساسية المشتركة ---
slice الأصلية: [10 20 30 40 50]
slice الفرعية: [20 30]

بعد تعديل slice الفرعية:
slice الأصلية: [10 999 30 40 50]
slice الفرعية: [999 30]

--- استخدام copy لنسخة مستقلة ---
slice الأصلية: [10 20 30 40 50]
النسخة المستقلة: [888 30 40]

--- دمج slices ---
نتيجة الدمج: [1 2 3 4 5 6]

مثال 3: التطبيق الشامل (الصعوبة ⭐⭐⭐)

GO
package main

import "fmt"

// removeElement يُزيل العنصر عند الفهرس المحدد من slice (يحافظ على الترتيب)
func removeElement(s []int, index int) []int {
	if index < 0 || index >= len(s) {
		return s // فهرس خارج النطاق، أعد slice الأصلية
	}
	// استخدام append لدمج الأجزاء قبل وبعد الفهرس
	return append(s[:index], s[index+1:]...)
}

// insertElement يُدخل عنصراً عند الفهرس المحدد
func insertElement(s []int, index int, value int) []int {
	if index < 0 || index > len(s) {
		return s
	}
	// توسيع slice أولاً، ثم تحويل العناصر، ثم إسناد
	s = append(s, 0)               // إضافة عنصر نائب
	copy(s[index+1:], s[index:])   // تحويل العناصر من index فصاعداً بمقدار واحد
	s[index] = value               // إسناد القيمة عند الهدف
	return s
}

// filterEven يُصفّي الأعداد الزوجية، يُعيد slice جديدة
func filterEven(s []int) []int {
	result := make([]int, 0, len(s)/2) // تقدير السعة كنصف
	for _, v := range s {
		if v%2 == 0 {
			result = append(result, v)
		}
	}
	return result
}

// اختبار مشاركة ذاكرة slice وآلية التوسع
func sliceInternals() {
	fmt.Println("=== عرض بنيات slice الداخلية ===")

	// إنشاء مصفوفة أساسية
	data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

	// قطع slice فرعية (تتشارك المصفوفة الأساسية)
	s1 := data[2:5] // [2, 3, 4]، len=3، cap=8
	s2 := s1[1:3]   // [3, 4]، len=2، cap=7

	fmt.Printf("data: %v, len=%d, cap=%d\n", data, len(data), cap(data))
	fmt.Printf("s1:   %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:   %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))

	// تعديل s2 للعناصر يؤثر على s1 و data
	s2[0] = 999
	fmt.Printf("\nبعد تعديل s2[0]=999:\n")
	fmt.Printf("data: %v\n", data) // data[4] تغيرت إلى 999
	fmt.Printf("s1:   %v\n", s1)   // s1[2] تغيرت إلى 999
	fmt.Printf("s2:   %v\n", s2)   // s2[0] تغيرت إلى 999

	// append قد يُسبب انفصالاً
	fmt.Println("\n--- append يُسبب توسع ---")
	s3 := data[0:2] // [0, 1]، len=2، cap=10
	fmt.Printf("قبل append s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))

	// append ضمن السعة، لا يزال مشاركاً
	s3 = append(s3, 99)
	fmt.Printf("بعد append(99) s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
	fmt.Printf("data[2] = %d (تُعدّلت!)\n", data[2])

	// بعد تجاوز السعة، تُخصص مصفوفة جديدة
	s3 = append(s3, 100, 200, 300, 400, 500, 600, 700, 800)
	fmt.Printf("بعد append كبير s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
	fmt.Printf("data لم تتأثر: %v\n", data)
}

func main() {
	// ========== عمليات slice عملية ==========
	fmt.Println("=== عمليات slice عملية ===")

	// البيانات الأولية
	nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	fmt.Println("البيانات الأصلية:", nums)

	// إزالة العنصر عند الفهرس 4 (قيمة 5)
	nums = removeElement(nums, 4)
	fmt.Println("بعد إزالة الفهرس 4:", nums)

	// إدراج 100 عند الفهرس 2
	nums = insertElement(nums, 2, 100)
	fmt.Println("بعد إدراج 100 عند الفهرس 2:", nums)

	// تصفية الأعداد الزوجية
	evens := filterEven(nums)
	fmt.Println("slice الأعداد الزوجية:", evens)

	// ========== slice كمكدس ==========
	fmt.Println("\n=== slice تحاكي مكدس ===")
	var stack []int

	// دفع
	for i := 1; i <= 5; i++ {
		stack = append(stack, i)
		fmt.Printf("دفع %d -> المكدس: %v\n", i, stack)
	}

	// سحب (من النهاية)
	for len(stack) > 0 {
		// الحصول على آخر عنصر
		top := stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		fmt.Printf("سحب %d -> المكدس: %v\n", top, stack)
	}

	// ========== عرض البنيات الداخلية ==========
	fmt.Println()
	sliceInternals()
}
▶ جرّب الكود

المخرجات:

TEXT
=== عمليات slice عملية ===
البيانات الأصلية: [1 2 3 4 5 6 7 8 9 10]
بعد إزالة الفهرس 4: [1 2 3 4 6 7 8 9 10]
بعد إدراج 100 عند الفهرس 2: [1 2 100 3 4 6 7 8 9 10]
slice الأعداد الزوجية: [2 100 4 6 8 10]

=== slice تحاكي مكدس ===
دفع 1 -> المكدس: [1]
دفع 2 -> المكدس: [1 2]
دفع 3 -> المكدس: [1 2 3]
دفع 4 -> المكدس: [1 2 3 4]
دفع 5 -> المكدس: [1 2 3 4 5]
سحب 5 -> المكدس: [1 2 3 4]
سحب 4 -> المكدس: [1 2 3]
سحب 3 -> المكدس: [1 2]
سحب 2 -> المكدس: [1]
سحب 1 -> المكدس: []

=== عرض بنيات slice الداخلية ===
data: [0 1 2 3 4 5 6 7 8 9], len=10, cap=10
s1:   [2 3 4], len=3, cap=8
s2:   [3 4], len=2, cap=7

بعد تعديل s2[0]=999:
data: [0 1 2 3 999 5 6 7 8 9]
s1:   [2 3 999]
s2:   [3 999]

--- append يُسبب توسع ---
قبل append s3: [0 1], len=2, cap=10
بعد append(99) s3: [0 1 99], len=3, cap=10
data[2] = 99 (تُعدّلت!)
بعد append كبير s3: [0 1 99 100 200 300 400 500 600 700 800], len=12, cap=20
data لم تتأثر: [0 1 99 3 999 5 6 7 8 9]

3. حالات استخدام شائعة

الحالة 1: معالجة البيانات بالدُفعات (تصفية وتحويل)

GO
package main

import "fmt"

// cleanData تُصفّي النصوص الفارغة من slice نصوص وتُحوّلها إلى أحرف كبيرة
func cleanData(input []string) []string {
	result := make([]string, 0, len(input))
	for _, s := range input {
		if s != "" {
			// تحويل إلى أحرف كبيرة (مثال مبسط، استخدم strings.ToUpper عملياً)
			upper := ""
			for _, c := range s {
				if c >= 'a' && c <= 'z' {
					upper += string(c - 32)
				} else {
					upper += string(c)
				}
			}
			result = append(result, upper)
		}
	}
	return result
}

func main() {
	raw := []string{"hello", "", "world", "", "go", "lang"}
	cleaned := cleanData(raw)
	fmt.Println("المنظفة:", cleaned) // [HELLO WORLD GO LANG]
}

الحالة 2: تنفيذ طابور ديناميكي

GO
package main

import "fmt"

// Queue يُنفذ طابور FIFO بسيطاً باستخدام slice
type Queue struct {
	items []string
}

// Enqueue يُضيف عنصراً إلى الطابور
func (q *Queue) Enqueue(item string) {
	q.items = append(q.items, item)
}

// Dequeue يُزيل و يُعيد أول عنصر
func (q *Queue) Dequeue() (string, bool) {
	if len(q.items) == 0 {
		return "", false
	}
	item := q.items[0]
	q.items = q.items[1:] // إزالة أول عنصر
	return item, true
}

// Size يُعيد حجم الطابور
func (q *Queue) Size() int {
	return len(q.items)
}

func main() {
	q := &Queue{}
	q.Enqueue("المهمة A")
	q.Enqueue("المهمة B")
	q.Enqueue("المهمة C")
	fmt.Printf("حجم الطابور: %d\n", q.Size())

	for q.Size() > 0 {
		item, _ := q.Dequeue()
		fmt.Println("معالجة:", item)
	}
}

❓ أسئلة شائعة

س1: هل slice المُعلنة بـ var s []int هي slice فارغة nil؟ هل يمكن استخدامها مباشرة؟

نعم. slice الفارغة nil لها len و cap بقيمة 0، و append يعمل بشكل مثالي. التوصية الرسمية لـ Go: إذا كان slice قد تكون فارغة، لا حاجة لفحص إضافي لـ nil قبل الاستخدام — فقط append مباشرة.

GO
var s []int           // slice فارغة nil
s = append(s, 1, 2)   // صالح تماماً
fmt.Println(s)        // [1 2]

س2: هل القطع بـ s[2:5] يؤثر على slice الأصلية؟

نعم. slice الفرعية الناتجة عن القطع تتشارك المصفوفة الأساسية مع الأصل. تعديل العناصر في slice الفرعية ينعكس على الأصلية. استخدم copy لنسخة مستقلة:

GO
original := []int{1, 2, 3, 4, 5}
sub := make([]int, 3)
copy(sub, original[2:5]) // نسخة مستقلة، لا تأثير متبادل

س3: لماذا تغيرت القيمة بعد append؟

عندما len(s) < cap(s)، يكتب append مباشرة في المصفوفة الأساسية دون إنشاء واحدة جديدة. إذا كانت slices أخرى تشير إلى مواقع لاحقة في نفس المصفوفة الأساسية، فسترى تغيرات "غير متوقعة". الحل: استخدم copy لإنشاء نسخة مستقلة قبل append.

س4: كيف تنمو سعة slice؟

يُحدد وقت تشغيل Go السعة الجديدة بناءً على سعة slice الحالية. الاستراتيجية النموذجية: مضاعفة السعة عندما تكون أقل من 1024، والنمو بمقدار 1.25x تقريباً عندما تكون أكبر من 1024. قد تتغير الاستراتيجية المحددة عبر إصدارات Go، لذلك لا تعتمد على قواعد نمو دقيقة.


📖 ملخص


📝 تمارين

تمرين 1 (⭐)

اكتب برنامجاً يُنشئ slice من 10 أعداد صحيحة (قيم 1~10)، ثم:

  1. اطبع طول slice وسعتها
  2. اقطع من الفهرس 2 إلى 7، اطبع محتوياتها وطولها وسعتها
  3. استخدم range للiterate على slice الفرعية، اطبع كل عنصر وفهرسه

تمرين 2 (⭐⭐)

اكتب دالة unique تقبل slice []int وتُعيد slice جديدة بإزالة التكرارات (مع الحفاظ على ترتيب الظهور الأول). مثلاً، المدخل [1, 3, 2, 3, 1, 4, 2] يجب أن يُعيد [1, 3, 2, 4].

تلميح: استخدم slice مساعدة لتسجيل العناصر التي ظهرت بالفعل.

تمرين 3 (⭐⭐⭐)

اكتب دالة mergeSorted تقبل sliceتين []int مُرتبتين وتدمجهما في slice واحدة مُرتبة. المتطلبات:


الدرس التالي

الدرس التالي: Map →

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%