Go: المصفوفات والشرائح في لغة Go
تُعد الشرائح جوهر الطريقة التي تتعامل بها لغة Go مع بيانات المجموعات — فهي «طرق عرض» خفيفة الوزن للمصفوفات، وقدرتها على تغيير الحجم ديناميكيًا تجعل عمليات المجموعات سريعة ومرنة في آن واحد.
نادرًا ما تُستخدم المصفوفات بشكل مباشر في لغة Go (لأن طولها ثابت)، لكن الشرائح موجودة في كل مكان. لفهم الشرائح، يجب عليك أولاً فهم المصفوفات — ففي جوهرها، الشريحة هي مؤشر إلى مصفوفة بالإضافة إلى طولها وسعتها. في هذا الدرس، ستتقن جميع المفاهيم الأساسية لأنواع المجموعات في لغة Go.
1. ستتعلم
- تعريف المصفوفات وتهيئتها والتكرار عبرها ومقارنتها
- 4 طرق لإنشاء شرائح (حرفية/مصفوفة/make/قيمة فارغة)
- الفرق بين
len()وcap() - معاني الحجج الثلاثة الخاصة بـ
make appendآلية تغيير الحجم تلقائيًا- شريحة
[low:high:max] - مخاطر مشاركة المصفوفة الأساسية في عملية التقطيع (نقطة أساسية)
- تنفيذ أداة تجميع بيانات ديناميكية باستخدام الشرائح
2. قصة حقيقية لمهندس جمع البيانات
(1) المشكلة: لا يمكن للمصفوفة ذات الحجم الثابت استيعاب البيانات الديناميكية
أليس هي مهندسة في مجال جمع البيانات. وقد احتاجت مؤخرًا إلى كتابة برنامج زحف على الويب:
"أحتاج إلى استرداد 10,000 سجل من كل مصدر من مصادر البيانات الخمسة وتخزينها في الذاكرة لإزالة التكرارات. في البداية، استخدمت قوائم Python — مجرد عملية «إلحاق» بسيطة وانتهى الأمر. بعد التحول إلى لغة Go، استخدمت مصفوفة ذات طول ثابت مثل
[10000]int، لكن البرنامج تعطل عند السجل الأول مباشرةً: «المؤشر خارج النطاق»."
فتحت شفرة لغة «Go» التي كتبتها:
// Version 1: Fixed-length array; cannot grow
var records [10000]int
for i := 0; i < 50000; i++ {
records[i] = i // ❌ i >= 10000 causes panic!
}
قال المدير: «يتغير عدد مصادر البيانات يوميًا، لذا فإن مصفوفتك ذات الطول الثابت لن تعمل». عندها فقط أدركت أليس أن مصفوفات لغة Go هي أنواع قيمية، وأن الطول جزء من النوع — فـ [5]int و [10]int نوعان مختلفان.
(2) الحل بلغة Go: استخدام الشرائح
// data_collector.go
package main
import "fmt"
func main() {
// Slices: Dynamic length, automatic resizing
var records []int // nil slice, can append
// Simulate retrieving 10,000 records from each of 5 data sources
for source := 1; source <= 5; source++ {
for i := 0; i < 10000; i++ {
value := source*10000 + i
records = append(records, value) // auto-resize
}
}
fmt.Printf("Total number of records: %d\n", len(records))
fmt.Printf("Underlying capacity: %d\n", cap(records))
fmt.Printf("Top 3: %v\n", records[:3])
fmt.Printf("Last 3: %v\n", records[len(records)-3:])
}
الناتج:
Total number of records: 50000
Underlying capacity: 65536
Top 3: [10000 10001 10002]
Last 3: [59997 59998 59999]
(3) الأداء: المصفوفة مقابل الشريحة
| البعد | مصفوفة [n]T |
شريحة []T |
|---|---|---|
| الطول | ثابت في وقت التحويل البرمجي | متغير في وقت التشغيل |
| النوع | [5]int ≠ [10]int |
[]int عام |
| تمرير المعلمات | تمرير القيمة (نسخ المصفوفة بأكملها) | تمرير المرجع (24 بايت فقط) |
| تغيير الحجم | غير مدعوم | append تلقائي |
| حالة الاستخدام | الحجم الثابت (مثل المخزن المؤقت) | 99% من الحالات |
3. المصفوفة
(1) تعريف المصفوفات وتهيئتها
package main
import "fmt"
func main() {
// Method 1: Declaration + Initialization
var a1 [5]int = [5]int{1, 2, 3, 4, 5}
// Method 2: Type Inference
a2 := [5]int{1, 2, 3, 4, 5}
// Method 3: Partial Initialization (with the Rest Set to Zero)
a3 := [5]int{1, 2} // [1 2 0 0 0]
// Method 4: Use ... to let the compiler infer the length
a4 := [...]int{1, 2, 3, 4} // length 4
// Method 5: Specifying Index Initialization
a5 := [5]int{0: 10, 4: 50} // [10 0 0 0 50]
fmt.Println(a1, a2, a3, a4, a5)
}
الناتج:
[1 2 3 4 5] [1 2 3 4 5] [1 2 0 0 0] [1 2 3 4] [10 0 0 0 50]
(2) التكرار عبر المصفوفات
package main
import "fmt"
func main() {
nums := [4]string{"Alice", "Bob", "Charlie", "Dave"}
// Method 1: for loop
for i := 0; i < len(nums); i++ {
fmt.Printf("%d: %s\n", i, nums[i])
}
// Method 2: for range
for i, name := range nums {
fmt.Printf("[%d] %s\n", i, name)
}
}
(3) المصفوفات هي أنواع قيمية
package main
import "fmt"
func modify(arr [3]int) {
arr[0] = 999 // modifies the copy
fmt.Printf("Inside function: %v\n", arr)
}
func main() {
a := [3]int{1, 2, 3}
modify(a)
fmt.Printf("Outside function: %v\n", a) // original array unchanged
}
الناتج:
Inside function: [999 2 3]
Outside function: [1 2 3]
▶ مثال: مقارنة المصفوفات (فقط للمصفوفات من نفس النوع والطول)
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2, 4}
fmt.Println(a == b) // true
fmt.Println(a == c) // false
// Items of different lengths or types cannot be compared.
// d := [4]int{1, 2, 3, 4}
// fmt.Println(a == d) // ❌ mismatched types
}
4. أربع طرق لإنشاء شرائح
(1) الشريحة = «عرض» لمصفوفة
graph TB
subgraph Slice["Slice (24 bytes)"]
P["ptr<br/>points to the underlying array"]
L["len<br/>length"]
C["cap<br/>capacity"]
end
subgraph Array["Underlying array"]
A0["[0]"]
A1["[1]"]
A2["[2]"]
A3["[3]"]
A4["[4]"]
A5["[5]"]
end
P --> A1
P -.-> A2
P -.-> A3
الشريحة هي مرجع إلى المصفوفة الأساسية — وهي تتكون من 24 بايتًا فقط (ptr + len + cap). ويمكن لعدة شرائح أن تتشارك في نفس المصفوفة الأساسية.
(2) الطريقة الأولى للتعريف: الإعلان المباشر عن شريحة ذات قيمة nil
var s []int // nil slice, len=0, cap=0
// You can append, but you cannot access it using `s[0]`.
(3) طريقة الإنشاء 2: حرفية
s := []int{1, 2, 3, 4, 5} // create and initialize
// Base array = [1 2 3 4 5], len = cap = 5
(4) طريقة الإنشاء 3: make (تحديد الطول والحد الأقصى)
// Syntax: make([]T, len, cap)
s := make([]int, 5) // len=5, cap=5, all zero values [0 0 0 0 0]
s := make([]int, 3, 10) // len=3, cap=10
▶ مثال: مقارنة بين 4 طرق لاستخدام الفعل "make"
package main
import "fmt"
func main() {
// Method 1: nil slice
var s1 []int
fmt.Printf("s1: len=%d cap=%d nil=%v\n", len(s1), cap(s1), s1 == nil)
// Method 2: Literal
s2 := []int{1, 2, 3}
fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)
// Method 3: Set the length
s3 := make([]int, 5)
fmt.Printf("s3: len=%d cap=%d %v\n", len(s3), cap(s3), s3)
// Method 4: make (length + capacity)
s4 := make([]int, 3, 10)
fmt.Printf("s4: len=%d cap=%d %v\n", len(s4), cap(s4), s4)
// Method 5: Slicing from an array
arr := [5]int{10, 20, 30, 40, 50}
s5 := arr[1:4] // [20 30 40], len=3 cap=4 (4 remaining elements)
fmt.Printf("s5: len=%d cap=%d %v\n", len(s5), cap(s5), s5)
}
الناتج:
s1: len=0 cap=0 nil=true
s2: len=3 cap=3 [1 2 3]
s3: len=5 cap=5 [0 0 0 0 0]
s4: len=3 cap=10 [0 0 0]
s5: len=3 cap=4 [20 30 40]
(6) مرجع سريع: 4 طرق للإنشاء
| الطريقة | الصيغة | حالات الاستخدام |
|---|---|---|
| شريحة فارغة | var s []T |
التهيئة المؤجلة |
| حرفي | []T{1, 2, 3} |
جميع العناصر معروفة |
| make | make([]T, len, cap) |
تخصيص السعة مسبقًا |
| تقطيع المصفوفات | arr[low:high] |
عرض المصفوفة الفرعية |
5. len() و cap()
(1) العلاقة بين len و cap
package main
import "fmt"
func main() {
s := make([]int, 3, 10) // len=3, cap=10
fmt.Printf("Initial: len=%d cap=%d\n", len(s), cap(s))
// len=3: You can access s[0], s[1], and s[2]
// cap=10: The underlying array has 10 elements, with room for 7 more to be appended
}
(2) معنى كلمة «cap»
cap - len = عدد العناصر التي لا يزال من الممكن إضافتها دون تغيير الحجم. إذا تجاوز هذا العدد cap، فسيقوم append بتخصيص مصفوفة أساسية جديدة (عادةً ما يضاعف الحجم).
▶ مثال: الفرق الفعلي بين len وcap
package main
import "fmt"
func main() {
s := make([]int, 0, 5) // len=0, cap=5
for i := 1; i <= 8; i++ {
s = append(s, i)
fmt.Printf("append %d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
}
}
الناتج:
append 1: len=1 cap=5 [1]
append 2: len=2 cap=5 [1 2]
append 3: len=3 cap=5 [1 2 3]
append 4: len=4 cap=5 [1 2 3 4]
append 5: len=5 cap=5 [1 2 3 4 5]
append 6: len=6 cap=10 [1 2 3 4 5 6] ← cap doubled
append 7: len=7 cap=10 [1 2 3 4 5 6 7]
append 8: len=8 cap=10 [1 2 3 4 5 6 7 8]
len == cap، يكون الحد الأقصى الجديد مساوياً للحد الأقصى القديم * 2 (للشرائح الصغيرة) أو للحد الأقصى القديم * 1.25 (للشرائح الكبيرة، التي يزيد عددها تقريباً عن 256).
6. تغيير الحجم الديناميكي القائم على الإضافة
(1) أساسيات append
package main
import "fmt"
func main() {
var s []int // nil
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
// Batch append: Splitting a slice
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
fmt.Println(s)
}
الناتج:
[1 2 3 4 5 6 7]
▶ مثال: حذف العناصر باستخدام append (بدون وظيفة delete المدمجة)
package main
import "fmt"
// Delete the element at index i
func removeAt(s []int, i int) []int {
// Concatenation: s[:i] + s[i+1:]
return append(s[:i], s[i+1:]...)
}
func main() {
s := []int{1, 2, 3, 4, 5}
s = removeAt(s, 2) // delete 3
fmt.Println(s) // [1 2 4 5]
}
الناتج:
[1 2 4 5]
(3) عيب append: قد يؤدي إلى تعديل المصفوفة الأصلية
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
s2 := s[:3] // [1 2 3], shares underlying array
s2 = append(s2, 99) // no resize, modified s's underlying array!
fmt.Println("s:", s) // [1 2 3 99 5]
fmt.Println("s2:", s2) // [1 2 3 99]
}
الناتج:
s: [1 2 3 99 5]
s2: [1 2 3 99]
append قد يؤثر على الشرائح الأخرى. استخدم copy() أو فرض تغيير الحجم عندما يتطلب الأمر عزلًا صارمًا.
7. شريحة [الحد الأدنى:الحد الأعلى:الحد الأقصى]
(1) ثلاثة أنواع من تعبيرات الشرائح
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
// Form 1: [low:high] — Takes [low, high)
s1 := arr[1:4]
fmt.Printf("arr[1:4] = %v, len=%d cap=%d\n", s1, len(s1), cap(s1))
// Format 2: [low:] — From low to the end
s2 := arr[2:]
fmt.Printf("arr[2:] = %v, len=%d cap=%d\n", s2, len(s2), cap(s2))
// Form 3: [:high] — From the beginning to high
s3 := arr[:3]
fmt.Printf("arr[:3] = %v, len=%d cap=%d\n", s3, len(s3), cap(s3))
// Format 4: [low:high:max] — Sets a cap (to prevent unexpected additions from affecting the original array)
s4 := arr[1:3:4]
fmt.Printf("arr[1:3:4] = %v, len=%d cap=%d\n", s4, len(s4), cap(s4))
}
الناتج:
arr[1:4] = [20 30 40], len=3 cap=4
arr[2:] = [30 40 50], len=3 cap=3
arr[:3] = [10 20 30], len=3 cap=5
arr[1:3:4] = [20 30], len=2 cap=3
(2) الغرض من [الحد الأدنى:الحد الأعلى:الحد الأقصى]
يضع max حدًا لمنع append من الكتابة فوق أجزاء أخرى من المصفوفة الأصلية:
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
// No limit on max: cap=4; after an append operation, arr[3] will be overwritten
s1 := arr[1:3] // [20 30], cap=4
s1 = append(s1, 999)
fmt.Println("arr:", arr) // [10 20 30 999 50] ← overwritten!
// Limit max=3: cap=2; append creates a new array
s2 := arr[1:3:3] // [20 30], cap=2
s2 = append(s2, 888)
fmt.Println("arr:", arr) // [10 20 30 999 50] ← unchanged
fmt.Println("s2:", s2) // [20 30 888]
}
الناتج:
arr: [10 20 30 999 50]
arr: [10 20 30 999 50]
s2: [20 30 888]
8. مخاطر مشاركة المصفوفة الأساسية عند التقطيع (نقطة أساسية)
(1) المأزق الأول: يؤدي تعديل عنصر شريحة إلى التأثير على جميع الشرائح
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[0:3] // [1 2 3]
s2 := arr[2:5] // [3 4 5]
s1[2] = 999 // same underlying array!
fmt.Println("s1:", s1) // [1 2 999]
fmt.Println("s2:", s2) // [999 4 5]
fmt.Println("arr:", arr) // [1 2 999 4 5]
}
(2) المشكلة الثانية: يؤدي استخدام append مع for range إلى حدوث حلقة لا نهائية
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
for _, v := range s {
s = append(s, v*10) // dangerous: s is growing
}
fmt.Println(s) // output is undefined
}
for range. قبل الإصدار 1.22 من لغة Go، كانت s في for i, v := range s تمثل لقطة لحجم الشريحة عند بداية الحلقة؛ وقد عالج الإصدار 1.22 من لغة Go هذه المشكلة في بعض الحالات، لكن لا يزال من المستحسن توخي الحذر.
(3) المأزق الثالث: يتم تعديل الشرائح التي يتم تمريرها كمعلمات للدالة بشكل غير متوقع
package main
import "fmt"
// Slicing uses pass-by-reference, so changes made within the function affect the outside.
func addElement(s []int) {
s = append(s, 100) // may modify the original array (if no resize)
fmt.Printf("Inside function: %v\n", s)
}
func main() {
s := make([]int, 3, 10)
s[0], s[1], s[2] = 1, 2, 3
addElement(s)
fmt.Printf("Outside function: %v\n", s) // depends on whether resize occurred
}
(4) استخدم copy() لحل مشكلات المشاركة
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[0:3]
// Create a separate copy
s2 := make([]int, len(s1))
copy(s2, s1)
s1[0] = 999
fmt.Println("s1:", s1) // [999 2 3]
fmt.Println("s2:", s2) // [1 2 3] ← unaffected
}
▶ مثال: استخدام "نسخ + إلحاق" لإدراج شريحة
package main
import "fmt"
// Insert element val at index i
func insert(s []int, i int, val int) []int {
// 1. Add 1 position
s = append(s, 0)
// 2. Shift the element at index i and all subsequent elements one position to the right
copy(s[i+1:], s[i:])
// 3. Add a new element
s[i] = val
return s
}
func main() {
s := []int{1, 2, 4, 5}
s = insert(s, 2, 3)
fmt.Println(s) // [1 2 3 4 5]
}
الناتج:
[1 2 3 4 5]
9. مثال كامل: أداة جمع البيانات الديناميكية
اجمع بين جميع ميزات الشرائح لإنشاء أداة لـ جمع البيانات من مصادر متعددة + إزالة التكرار + التصفية:
// collector.go
package main
import (
"fmt"
"strings"
)
// Data Source Simulation Functions
func fetchFromSource(sourceName string) []string {
data := map[string][]string{
"GitHub": {"repo:go", "repo:docker", "issue:bug", "repo:go"}, // last one is duplicate
"NPM": {"pkg:react", "pkg:vue", "pkg:react"}, // duplicate
"PyPI": {"pkg:requests", "pkg:flask"},
}
return data[sourceName]
}
// Removing Duplicates: Using `map` to Create a Hash Set
func deduplicate(data []string) []string {
seen := make(map[string]bool)
result := make([]string, 0, len(data))
for _, item := range data {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// Filter: Keep only those starting with "repo:"
func filterRepos(data []string) []string {
result := make([]string, 0)
for _, item := range data {
if strings.HasPrefix(item, "repo:") {
result = append(result, item)
}
}
return result
}
// Pipeline: Combining Multiple Steps
func collectPipeline() (total, unique, repos int, sources []string) {
defer func() {
// Naming Return Values + Defer Collection Statistics
fmt.Printf("\n[Collection Complete] total=%d unique=%d repos=%d sources=%v\n",
total, unique, repos, sources)
}()
allSources := []string{"GitHub", "NPM", "PyPI"}
var allData []string
for _, src := range allSources {
sources = append(sources, src)
data := fetchFromSource(src)
allData = append(allData, data...)
total += len(data)
}
uniqueData := deduplicate(allData)
unique = len(uniqueData)
repoData := filterRepos(uniqueData)
repos = len(repoData)
fmt.Println("\n=== Deduped Data ===")
for _, item := range uniqueData {
fmt.Printf(" %s\n", item)
}
fmt.Println("\n=== Filtering Repo Data ===")
for _, item := range repoData {
fmt.Printf(" %s\n", item)
}
return total, unique, repos, sources
}
func main() {
collectPipeline()
}
النتيجة المتوقعة:
=== Deduped Data ===
repo:go
repo:docker
issue:bug
pkg:react
pkg:vue
pkg:requests
pkg:flask
=== Filtering Repo Data ===
repo:go
repo:docker
[Collection Complete] total=9 unique=7 repos=2 sources=[GitHub NPM PyPI]
make([]string, 0, len(data)) لتخصيص السعة مسبقًا وتجنب تغيير الحجم بشكل متكرر باستخدام append. هذه تقنية أساسية لتحسين الأداء.
❓ أسئلة شائعة
append دائمًا إلى تغيير حجم المصفوفة؟len == cap. أما إذا كانت هناك مساحة متوفرة، يتم كتابة العناصر مباشرةً إلى المصفوفة الأصلية.append يؤثر على الشريحة الخارجية أم لا يعتمد على ما إذا كان قد تم تغيير حجم الشريحة أم لا.max بالصيغة [low:high:max] مطلوبًا؟append من التأثير على العناصر الأخرى في المصفوفة الأصلية. سيناريو شائع: إنشاء شريحة فرعية من شريحة أكبر، ثم إجراء عملية append.list.remove(i). بل تُنفذ لغة Go عملية الإزالة باستخدام append(s[:i], s[i+1:]...) (ولكن هذا لا يحافظ على الترتيب). إذا كنت ترغب في الحفاظ على الترتيب، فستحتاج إلى إعادة ترتيب العناصر يدويًّا.copy(dst, src) — تنسخ min(len(dst), len(src)) عنصرًا؛ (2) append([]T(nil), src...) — تنسخ جميع العناصر؛ (3) يدويًّا باستخدام for range.range؟range على مصفوفة إلى نسخ المصفوفة بأكملها (أداء ضعيف)؛ بينما يؤدي استخدام range على شريحة إلى نسخ 24 بايت فقط (أداء جيد). ولهذا السبب تُعد الشرائح المعيار الفعلي لـ«المجموعات».📖 ملخص
- المصفوفات هي أنواع قيمية (ذات طول ثابت، تُمرَّر عن طريق النسخ)، في حين أن الشرائح هي أنواع مرجعية (ديناميكية، تُمرَّر عن طريق مشاركة البيانات الأساسية).
- 4 طرق لإنشاء شرائح: nil / القيم الثابتة / make / شرائح المصفوفات
lenهو عدد العناصر المرئية، وcapهي سعة المصفوفة الأساسية، وappendتقوم بتغيير حجم المصفوفة عندlen == cap(استراتيجية المضاعفة)- تعبير الشريحة ذو المعلمات الثلاثة
[low:high:max]يضع حدًا علىcapلمنع عمليات الإضافة خارج النطاق - يؤدي تقاسم المصفوفة الأساسية على شكل شرائح إلى ثلاث مشكلات شائعة: تأثير التعديلات التي تُجرى على العناصر على بعضها البعض / الإضافة داخل
range/ الآثار الجانبية للإضافة باستخدام الدوال - الحلول:
copy()لإنشاء نسخة منفصلة / فرض تغيير الحجم (append(s, 0)[:0]) / تحديد الحد الأقصى - في المشاريع الفعلية، تُستخدم الشرائح الزمنية في 99% من الحالات؛ أما المصفوفات فلا تُستخدم إلا في السيناريوهات التي تنطوي على أحجام ثابتة أو دلالات القيم.
📝 تمارين
-
المشكلة الأساسية (الصعوبة ⭐): استخدم الشرائح لتنفيذ دالة
reverse([]int) []intتعمل على عكس ترتيب عناصر الشريحة. تلميح: استخدم حلقةforمع متغير مؤقت، أو قم بتبديل العناصر في مكانها. حالة الاختبار:reverse([]int{1,2,3,4,5})→[5 4 3 2 1]. -
مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ الدالة
uniqueSorted(nums []int) []int: إزالة التكرارات والترتيب تصاعديًا. تلميح: قم أولاً بترتيب المصفوفة باستخدامsort.Ints، ثم قم بالتكرار عبرها لإزالة التكرارات. إذا كان المدخلات هي[]int{3, 1, 4, 1, 5, 9, 2, 6, 5}، فيجب أن تكون المخرجات[1 2 3 4 5 6 9]. -
مشكلة التحدي (درجة الصعوبة ⭐⭐⭐): قم بتنفيذ خوارزمية الحد الأقصى للنافذة المنزلقة:
maxSlidingWindow(nums []int, k int) []int. على سبيل المثال،nums=[1,3,-1,-3,5,3,6,7], k=3→ الناتج[3,3,5,5,6,7]. المتطلبات: (1) التعقيد الزمني O(n)؛ (2) استخدمdeque(الذي يمكن محاكاته باستخدام[]int) للحفاظ على النافذة؛ (3) اشرح لماذا سيؤدي التمرير المباشر إلى انتهاء المهلة (O(n*k)).