المصفوفات و 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. الصياغة والاستخدام الأساسي
إعلان وتهيئة المصفوفات
// إعلان مصفوفة 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
// إعلان 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
nil و slices الفارغة ([]int{} أو make([]int, 0)) متكافئة وظيفياً — len و cap كلاهما 0، و append يعمل بشكل طبيعي. الفرق هو أن slices الفارغة nil تُسلسل إلى null في JSON، بينما slices الفارغة تُسلسل إلى [].
عمليات Slice
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: الاستخدام الأساسي (الصعوبة ⭐)
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))
}
المخرجات:
مصفوفة الدرجات: [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: الاستخدام المتوسط (الصعوبة ⭐⭐)
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)
}
المخرجات:
الأولية: 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: التطبيق الشامل (الصعوبة ⭐⭐⭐)
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()
}
المخرجات:
=== عمليات 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: معالجة البيانات بالدُفعات (تصفية وتحويل)
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: تنفيذ طابور ديناميكي
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 مباشرة.
var s []int // slice فارغة nil
s = append(s, 1, 2) // صالح تماماً
fmt.Println(s) // [1 2]
س2: هل القطع بـ s[2:5] يؤثر على slice الأصلية؟
نعم. slice الفرعية الناتجة عن القطع تتشارك المصفوفة الأساسية مع الأصل. تعديل العناصر في slice الفرعية ينعكس على الأصلية. استخدم copy لنسخة مستقلة:
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، لذلك لا تعتمد على قواعد نمو دقيقة.
📖 ملخص
- المصفوفة
[N]Tلها طول ثابت، نوع قيمة؛ الإسناد/تمرير المعاملات ينسخ المصفوفة بالكامل - Slice
[]Tلها طول متغير، نوع مرجع؛ تشير إلى مصفوفة أساسية make([]T, len, cap)هي الطريقة المُوصى بها لإنشاء slices؛ تخصيص السعة مسبقاً يتجنب التوسع المتكررappendيُعيد slice جديدة؛ يجب التقاط قيمة الإرجاع؛ يُخصص مصفوفة أساسية جديدة عند عدم كفاية السعة- قطع slice
s[low:high]يتشارك المصفوفة الأساسية مع الأصل؛ التغييرات تؤثر على بعضها copy(dst, src)يُنشئ نسخة مستقلة من slicelen()يُعيد عدد العناصر؛cap()يُعيد سعة المصفوفة الأساسية- القيمة الصفرية لـ slice هي
nil؛ يمكنكappendمباشرة
📝 تمارين
تمرين 1 (⭐)
اكتب برنامجاً يُنشئ slice من 10 أعداد صحيحة (قيم 1~10)، ثم:
- اطبع طول slice وسعتها
- اقطع من الفهرس 2 إلى 7، اطبع محتوياتها وطولها وسعتها
- استخدم 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 واحدة مُرتبة. المتطلبات:
- تعقيد الوقت يجب أن يكون O(n)؛ لا تدمج ثم ترتيب
- مثلاً، المدخل
[1, 3, 5, 7]و[2, 4, 6, 8]يجب أن يُعيد[1, 2, 3, 4, 5, 6, 7, 8] - فكر: أي دور يلعب هذا_OPERATION في خوارزمية الدمج؟



