Skip to content

slice

TIP

قراءة هذه المقالة تتطلب معرفة بمكتبة unsafe القياسية.

الشريحة يجب أن تكون هيكل البيانات الأكثر استخدامًا في لغة Go، بلا منازع (في الواقع الهياكل المدمجة قليلة)، يمكن رؤيتها في كل مكان تقريبًا. استخداماتها الأساسية ذُكرت في مقدمة اللغة، الآن لنرَ شكلها الداخلي وكيف تعمل.

البنية

تنفيذ الشريحة موجود في ملف runtime/slice.go. في وقت التشغيل، الشريحة موجودة كبنية، نوعها runtime.slice، كما يلي:

go
type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}

هذه البنية تحتوي على ثلاثة حقول فقط:

  • array، مؤشر للمصفوفة الأساسية
  • len، طول الشريحة، يشير لعدد العناصر الموجودة في المصفوفة
  • cap، سعة الشريحة، تشير لإجمالي عدد العناصر التي يمكن للمصفوفة استيعابها

من المعلومات أعلاه، يتضح أن التنفيذ الأساسي للشريحة يعتمد على المصفوفات. عادة هي مجرد بنية تحمل مرجعًا للمصفوفة، وسجلات للسعة والطول. بهذا تكون تكلفة تمرير الشريحة منخفضة جدًا، فقط نسخ مرجع البيانات، وليس نسخ جميع البيانات. وعند استخدام len و cap للحصول على طول وسعة الشريحة، يكون ذلك مساويًا للحصول على قيم حقولها، دون الحاجة لاجتياز المصفوفة.

لكن هذا أيضًا يجلب بعض المشاكل غير الواضحة، انظر المثال التالي:

go
package main

import "fmt"

func main() {
  s := make([]int, 0, 10)
  s = append(s, 1, 2, 3, 4, 5)
  s1 := s[:]
  s1[0] = 2
  fmt.Println(s)
}
[2 2 3 4 5]

في الكود أعلاه، s1 أُنشئت بتقطيع شريحة جديدة، لكنها والشريحة الأصلية يشيران لنفس المصفوفة الأساسية. تعديل بيانات s1 سيؤدي لتغير s أيضًا. لذا عند نسخ الشريحة يجب استخدام دالة copy، الشريحة المنسوخة لاحقًا لا علاقة لها بالأولى. انظر مثالًا آخر:

go
func main() {
  s := make([]int, 0, 10)
  s = append(s, 1, 2, 3, 4, 5)
  s1 := s[:]
  s1 = append(s1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  s1[0] = 10
  fmt.Println(s)
  fmt.Println(s1)
}
[1 2 3 4 5]
[10 2 3 4 5 1 2 3 4 5 6 7 8 9 10]

نفس طريقة النسخ بالتقطيع، لكن هذه المرة لن تؤثر على الشريحة الأصلية. مبدئيًا s1 و s كانا يشيران لنفس المصفوفة، لكن لاحقًا أُضيفت لعناصر s1 أكثر مما تستوعبه المصفوفة، فتم تخصيص مصفوفة جديدة أكبر، لذا في النهاية أصبحا يشيران لمصفوفتين مختلفتين. هل تعتقد أن المشكلة انتهت؟ انظر مثالًا آخر:

go
package main

import "fmt"

func main() {
  s := make([]int, 0, 10)
  appendData(s, 1, 2, 3, 4, 5, 6)
  fmt.Println(s)
}

func appendData[T comparable](s []T, data ...T) {
  s = append(s, data...)
}
[]

أُضيفت العناصر، لكن المطبوع شريحة فارغة. في الواقع البيانات أُضيفت للشريحة، لكنها كُتبت في المصفوفة الأساسية. معاملات الدوال في Go تُمرر بالقيمة، لذا المعامل s هو نسخة من بنية الشريحة الأصلية، و append بعد إضافة العناصر تُرجع بنية شريحة مُحدَّثة الطول، لكن الإسناد يكون للمعامل s وليس الشريحة الأصلية s، ولا علاقة بينهما.

للشريحة، موقع البداية للوصول والتعديل يعتمد على موقع مرجع المصفوفة، والإزاحة تعتمد على الطول المسجل في البنية. المؤشر في البنية يمكن أن يشير للبداية، أو لمنتصف المصفوفة، كما في الصورة التالية.

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

go
s := make([]int, 0, 10)
s1 := s[:4]
s2 := s[4:6]
s3 := s[7:]

عند التقطيع، سعة الشريحة الجديدة تساوي طول المصفوفة ناقص موقع بداية مرجع الشريحة الجديدة. مثلاً s[4:6] سعة الشريحة الجديدة هي 6 = 10 - 4. بالطبع، نطاق مرجع الشريحة لا يجب أن يكون متجاورًا، يمكن أن يتداخل، لكن هذا يسبب مشاكل كبيرة، فقد تُعدَّل بيانات شريحة حالية دون علم من شريحة أخرى. مثلاً الشريحة البنفسجية في الصورة، إذا استُخدمت append لإضافة عناصر لاحقًا، قد تُغطي بيانات الشريحة الخضراء والزرقاء. لتجنب هذا، تسمح Go بتحديد نطاق السعة عند التقطيع، كالتالي:

go
s4 = s[4:6:6]

في هذه الحالة، سعتها محدودة بـ 2، فإضافة عناصر ستثير التوسع، وبعد التوسع تكون مصفوفة جديدة، لا علاقة لها بالمصفوفة الأصلية. هل تعتقد أن مشاكل الشرائح انتهت؟ لا، انظر مثالًا آخر:

go
package main

import "fmt"

func main() {
  s := make([]int, 0, 10)
  // عدد العناصر المضافة أكبر من السعة مباشرة
  appendData(s, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
  fmt.Println(s)
}

func appendData[T comparable](s []T, data ...T) {
  s = append(s, data...)
}
[]

الكود نفسه، لكن بعدل المعاملات لجعل عدد العناصر المضافة أكبر من سعة الشريحة، مما يثير التوسع. بهذا البيانات لم تُضف للشريحة الأصلية s، وحتى المصفوفة الأساسية لم تُكتب فيها بيانات. يمكن التحقق بمؤشر unsafe:

go
package main

import (
  "fmt"
  "unsafe"
)

func main() {
  s := make([]int, 0, 10)

  // عدد العناصر المضافة أكبر من السعة مباشرة
  appendData(s, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
  fmt.Println("ori slice", unsafe.SliceData(s))
  unsafeIterator(unsafe.Pointer(unsafe.SliceData(s)), cap(s))
}

func appendData[T comparable](s []T, data ...T) {
  s = append(s, data...)
  fmt.Println("new slice", unsafe.SliceData(s))
  unsafeIterator(unsafe.Pointer(unsafe.SliceData(s)), cap(s))
}

func unsafeIterator(ptr unsafe.Pointer, offset int) {
  for ptr, i := ptr, 0; i < offset; ptr, i = unsafe.Add(ptr, unsafe.Sizeof(int(0))), i+1 {
    elem := *(*int)(ptr)
    fmt.Printf("%d, ", elem)
  }
  fmt.Println()
}
new slice 0xc0000200a0
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0,
ori slice 0xc000018190
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,

المصفوفة الأساسية للشريحة الأصلية فارغة، البيانات كُتبت في المصفوفة الجديدة، لكن لا علاقة لها بالشريحة الأصلية. الشريحة كبنية تجعلها خفيفة جدًا، لكن المشاكل أعلاه لا يمكن تجاهلها، خاصة في الكود الفعلي حيث تكون هذه المشاكل مخفية بعمق ويصعب اكتشافها.

الإنشاء

في وقت التشغيل، إنشاء الشريحة بدالة make يتم بواسطة runtime.makeslice، منطقها بسيط، توقيع الدالة:

go
func makeslice(et *_type, len, cap int) unsafe.Pointer

تستقبل ثلاثة معاملات: نوع العنصر، الطول، السعة، وتُرجع مؤشر للمصفوفة الأساسية. الكود:

go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
    // حساب الذاكرة الإجمالية المطلوبة، إذا كانت كبيرة جدًا ستسبب تجاوزًا رقميًا
    // mem = sizeof(et) * cap
  mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
  if overflow || mem > maxAlloc || len < 0 || len > cap {
        // mem = sizeof(et) * len
    mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
    if overflow || mem > maxAlloc || len < 0 {
      panicmakeslicelen()
    }
    panicmakeslicecap()
  }

    // إذا لا توجد مشاكل، خصص الذاكرة
  return mallocgc(mem, et, true)
}

المنطق بسيط جدًا، أمران فقط:

  • حساب الذاكرة المطلوبة
  • تخصيص مساحة الذاكرة

إذا فشل فحص الشروط، سيحدث panic مباشرة:

  • تجاوز رقمي عند حساب الذاكرة
  • النتيجة أكبر من أقصى ذاكرة يمكن تخصيصها
  • الطول والسعة غير صالحين

إذا كانت الذاكرة المحسوبة أكبر من 32KB، ستُخصص على الكومة، ثم يُرجع مؤشر للمصفوفة الأساسية. بناء بنية runtime.slice لا تتم بواسطة دالة makeslice. في الواقع، بناء البنية يتم في وقت الترجمة، ودالة makeslice في وقت التشغيل مسؤولة فقط عن تخصيص الذاكرة، مثل الكود التالي:

go
var s runtime.slice
s.array = runtime.makeslice(type,len,cap)
s.len = len
s.cap = cap

إذا كنت مهتمًا يمكنك مراجعة الكود الوسيط المولد.

إذا استُخدمت مصفوفة لإنشاء شريحة، مثل:

go
var arr [5]int
s := arr[:]

هذه العملية تشبه الكود التالي:

go
var arr [5]int
var s runtime.slice
s.array = &arr
s.len = len
s.cap = cap

ستستخدم Go المصفوفة مباشرة كمصفوفة أساسية للشريحة، لذا تعديل بيانات الشريحة سيؤثر على بيانات المصفوفة. عند استخدام مصفوفة لإنشاء شريحة، الطول يساوي high-low، والسعة تساوي max-low، حيث max افتراضيًا طول المصفوفة، أو يمكن تحديد السعة يدويًا عند التقطيع، مثلاً:

go
var arr [5]int
s := arr[2:3:4]

الوصول

الوصول للشريحة مثل الوصول للمصفوفة باستخدام فهرس:

go
elem := s[i]

عملية الوصول للشريحة تتم في وقت الترجمة، بتوليد كود وسيط، الكود المولد النهائي يمكن فهمه كالكود الزائف التالي:

go
p := s.ptr
e := *(p + sizeof(elem(s)) * i)

في الواقع يتم الوصول للعنصر عبر تحريك المؤشر. في دالة cmd/compile/internal/ssagen.exprCheckPtr:

go
case ir.OINDEX:
    n := n.(*ir.IndexExpr)
    switch {
    case n.X.Type().IsSlice():
        // إزاحة المؤشر
        p := s.addr(n)
        return s.load(n.X.Type().Elem(), p)

عند استخدام len و cap للوصول لطول وسعة الشريحة، نفس الأمر، في دالة cmd/compile/internal/ssagen.exprCheckPtr:

go
case ir.OLEN, ir.OCAP:
    n := n.(*ir.UnaryExpr)
    switch {
    case n.X.Type().IsSlice():
        op := ssa.OpSliceLen
        if n.Op() == ir.OCAP {
            op = ssa.OpSliceCap
        }
        return s.newValue1(op, types.Types[types.TINT], s.expr(n.X))

في الكود المولد فعليًا، يتم الوصول لحقل len في بنية الشريحة بتحريك المؤشر، يمكن فهمه كالكود الزائف التالي:

go
p := &s
len := *(p + 8)
cap := *(p + 16)

إذا كان الكود التالي:

go
func lenAndCap(s []int) (int, int) {
  l := len(s)
  c := cap(s)
  return l, c
}

ففي مرحلة ما من الكود الوسيط المولد سيكون شكله تقريبًا:

go
v9 (+9) = ArgIntReg <int> {s+8} [1] : BX (l[int], s+8[int])
v10 (+10) = ArgIntReg <int> {s+16} [2] : CX (c[int], s+16[int])
v1 (?) = InitMem <mem>
v3 (11) = Copy <int> v9 : AX
v4 (11) = Copy <int> v10 : BX
v11 (+11) = MakeResult <int,int,mem> v3 v4 v1 : <>
Ret v11 (+11)
name l[int]: v9
name c[int]: v10
name s+16[int]: v10
name s+8[int]: v9

من أعلاه يتضح، أحدهما +8 والآخر +16، بوضوح الوصول لحقول الشريحة بتحريك المؤشر.

إذا أمكن استنتاج الطول والسعة في وقت الترجمة، لن يكون هناك حاجة لتحريك المؤشر في وقت التشغيل، مثل:

go
s := make([]int, 10, 20)
l := len(s)
c := cap(s)

قيم المتغيرين l و c ستُستبدل مباشرة بـ 10 و 20.

الكتابة

التعديل

go
s := make([]int, 10)
s[0] = 100

عند تعديل قيمة الشريحة عبر فهرس، في وقت الترجمة سيُولد كود مشابه للكود الزائف التالي عبر عملية OpStore:

go
p := &s
l := *(p + 8)
if !IsInBounds(l,i) {
    panic()
}
ptr := (s.ptr + i * sizeof(elem) * i)
*ptr = val

في مرحلة ما من الكود الوسيط:

go
v1 (?) = InitMem <mem>
v5 (8) = Arg <[]int> {s} (s[[]int])
v6 (?) = Const64 <int> [100]
v7 (?) = Const64 <int> [0]
v8 (+9) = SliceLen <int> v5
v9 (9) = IsInBounds <bool> v7 v8
v14 (?) = Const64 <int64> [0]
v12 (9) = SlicePtr <*int> v5
v15 (9) = Store <mem> {int} v12 v6 v1
v11 (9) = PanicBounds <mem> [0] v7 v7 v1
Exit v11 (9)

name s[[]int]: v5
name s[*int]:
name s+8[int]:

يمكن ملاحظة أن الكود يصل لطول الشريحة للتحقق من صحة الفهرس، وأخيرًا يخزن العنصر بتحريك المؤشر.

الإضافة

بدالة append يمكن إضافة عناصر للشريحة:

go
var s []int
s = append(s, 1, 2, 3)

بعد إضافة العناصر، تُرجع بنية شريحة جديدة. إذا لم يحدث توسع فالفرق فقط تحديث الطول، وإلا ستشير لمصفوفة جديدة. استخدامات append وُضحت بالتفصيل في قسم البنية. الآن سنركز على كيفية عمل append.

في وقت التشغيل، لا توجد دالة مثل runtime.appendslice. إضافة العناصر تتم في وقت الترجمة، دالة append تُوسع لكود وسيط. كود الحكم في دالة cmd/compile/internal/walk/assign.go walkassign:

go
case ir.OAPPEND:
    // x = append(...)
    call := as.Y.(*ir.CallExpr)
    if call.Type().Elem().NotInHeap() {
       base.Errorf("%v can't be allocated in Go; it is incomplete (or unallocatable)", call.Type().Elem())
    }
    var r ir.Node
    switch {
    case isAppendOfMake(call):
       // x = append(y, make([]T, y)...)
       r = extendSlice(call, init)
    case call.IsDDD:
       r = appendSlice(call, init) // also works for append(slice, string).
    default:
       r = walkAppend(call, init, as)
    }

ثلاث حالات:

  • إضافة عدة عناصر
  • إضافة شريحة
  • إضافة شريحة مؤقتة

سنوضح شكل الكود المولد.

إضافة عناصر

go
s = append(s, x, y, z)

إذا كان عدد العناصر محدودًا، ستُوسع بواسطة walkAppend للكود التالي:

go
// عدد العناصر المراد إضافتها
const argc = len(args) - 1
newLen := s.len + argc

// هل يحتاج توسع
if uint(newLen) <= uint(s.cap) {
  s = s[:newLen]
} else {
  s = growslice(s.ptr, newLen, s.cap, argc, elemType)
}

s[s.len - argc] = x
s[s.len - argc + 1] = y
s[s.len - argc + 2] = z

أولًا حساب عدد العناصر المراد إضافتها، ثم الحكم على الحاجة للتوسع، وأخيرًا الإسناد واحدًا تلو الآخر.

إضافة شريحة

go
s = append(s, s1...)

إذا أُضيف شريحة مباشرة، ستُوسع بواسطة appendSlice:

go
newLen := s.len + s1.len
// Compare as uint so growslice can panic on overflow.
if uint(newLen) <= uint(s.cap) {
  s = s[:newLen]
} else {
  s = growslice(s.ptr, s.len, s.cap, s1.len, T)
}
memmove(&s[s.len-s1.len], &s1[0], s1.len*sizeof(T))

نفس الأمر: حساب الطول الجديد، الحكم على التوسع، الاختلاف أن Go لا تضيف عناصر الشريحة المصدر واحدًا تلو الآخر، بل تختار نسخ الذاكرة مباشرة.

إضافة شريحة مؤقتة

go
s = append(s, make([]T, l2)...)

إذا أُضيف شريحة مؤقتة، ستُوسع بواسطة extendslice:

go
if l2 >= 0 {
// Empty if block here for more meaningful node.SetLikely(true)
} else {
  panicmakeslicelen()
}
s := l1
n := len(s) + l2

if uint(n) <= uint(cap(s)) {
  s = s[:n]
} else {
  s = growslice(T, s.ptr, n, s.cap, l2, T)
}
// clear the new portion of the underlying array.
hp := &s[len(s)-l2]
hn := l2 * sizeof(T)
memclr(hp, hn)

للشريحة المؤقتة، تحصل Go على طولها، وإذا كانت سعة الشريحة الحالية غير كافية، تحاول التوسع، ثم تمسح ذاكرة الجزء المقابل.

التوسع

من قسم البنية، التنفيذ الأساسي للشريحة هو مصفوفة. المصفوفة هيكل بيانات ثابت الطول، لكن طول الشريحة متغير. عندما لا تكفي سعة المصفوفة، تُطلب مساحة ذاكرة أكبر لتخزين البيانات، أي مصفوفة جديدة، ثم تُنسخ البيانات القديمة، ويشير مرجع الشريحة للمصفوفة الجديدة. هذه العملية تسمى التوسع. التوسع في وقت التشغيل يتم بواسطة runtime.growslice:

go
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice

شرح بسيط للمعاملات:

  • oldPtr، مؤشر للمصفوفة القديمة
  • newLen، طول المصفوفة الجديدة، newLen = oldLen + num
  • oldCap، سعة الشريحة القديمة، تساوي طول المصفوفة القديمة
  • et، نوع العنصر

القيمة المرجعة هي شريحة جديدة، لا علاقة لها بالشريحة الأصلية، الاشتراك الوحيد هو أن البيانات المحفوظة متطابقة.

go
var s []int
s = append(s, elems...)

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

عند التوسع، أولًا تحديد الطول والسعة الجديدين:

go
oldLen := newLen - num
if newLen < 0 {
    panic(errorString("growslice: len out of range"))
}

if et.Size_ == 0 {
    return slice{unsafe.Pointer(&zerobase), newLen, newLen}
}

newcap := oldCap
// سعة مضاعفة
doublecap := newcap + newcap
if newLen > doublecap {
    newcap = newLen
} else {
    const threshold = 256
    if oldCap < threshold {
        newcap = doublecap
    } else {
        for 0 < newcap && newcap < newLen {
            // newcap += 0.25 * newcap + 192
            newcap += (newcap + 3*threshold) / 4
        }
        // تجاوز رقمي
        if newcap <= 0 {
            newcap = newLen
        }
    }
}

من الكود أعلاه، للشرائح بسعة أقل من 256، السعة تتضاعف. للشرائح بسعة 256 أو أكثر، ستكون السعة الجديدة على الأقل 1.25 ضعف السعة الأصلية. عندما تكون الشريحة صغيرة، التضاعف في كل مرة يتجنب التوسع المتكرر. عندما تكون كبيرة، معدل التوسع يقل لتجنب طلب ذاكرة زائدة.

بعد الحصول على الطول والسعة الجديدين، حساب الذاكرة المطلوبة:

go
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
    ...
    ...
  default:
    lenmem = uintptr(oldLen) * et.Size_
    newlenmem = uintptr(newLen) * et.Size_
    capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
    capmem = roundupsize(capmem)
    // السعة النهائية
    newcap = int(capmem / et.Size_)
    capmem = uintptr(newcap) * et.Size_
}

if overflow || capmem > maxAlloc {
    panic(errorString("growslice: len out of range"))
}

معادلة حساب الذاكرة: mem = cap * sizeof(et). لتسهيل محاذاة الذاكرة، تُقرَّب للأسفل لأقرب قوة 2، ويُعاد حساب السعة الجديدة. إذا كانت السعة الجديدة كبيرة جدًا وتسببت في تجاوز رقمي، أو تجاوزت أقصى ذاكرة يمكن تخصيصها، سيحدث panic.

go
var p unsafe.Pointer
// تخصيص الذاكرة
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)

memmove(p, oldPtr, lenmem)
return slice{p, newLen, newcap}

بعد حساب النتائج، تُخصص ذاكرة بالحجم المحدد، تُمسح الذاكرة من newLen إلى newCap، تُنسخ بيانات المصفوفة القديمة للشريحة الجديدة، وأخيرًا تُبنى بنية الشريحة.

النسخ

go
src := make([]int, 10)
dst := make([]int, 20)
copy(dst, src)

عند استخدام copy لنسخ شريحة، تُحدد cmd/compile/internal/walk.walkcopy في وقت الترجمة كيفية النسخ. إذا كان الاستدعاء في وقت التشغيل، تُستخدم runtime.slicecopy:

go
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int

تستقبل مؤشر وطول الشريحة المصدر والهدف، وطول النسخ width. منطق الدالة بسيط جدًا:

go
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
  if fromLen == 0 || toLen == 0 {
    return 0
  }

  n := fromLen
  if toLen < n {
    n = toLen
  }

  if width == 0 {
    return n
  }

  // حساب عدد البايتات المراد نسخها
  size := uintptr(n) * width

  if size == 1 {
    *(*byte)(toPtr) = *(*byte)(fromPtr)
  } else {
    memmove(toPtr, fromPtr, size)
  }
  return n
}

قيمة width تعتمد على أصغر طول للشريحتين. يمكن ملاحظة أن النسخ لا يتم باجتياز العناصر واحدًا تلو الآخر، بل بنسخ ذاكرة المصفوفة الأساسية مباشرة. عندما تكون الشريحة كبيرة، نسخ الذاكرة يؤثر على الأداء.

إذا لم يكن الاستدعاء في وقت التشغيل، سيُوسع للكود التالي:

go
n := len(a)
if n > len(b) {
  n = len(b)
}
if a.ptr != b.ptr {
  memmove(a.ptr, b.ptr, n*sizeof(elem(a)))
}

كلا الطريقتين نفس المبدأ: نسخ الشريحة بنسخ الذاكرة. دالة memmove منفذة بالتجميع.

المسح

go
package main

func main() {
  s := make([]int, 0, 10)
  s = append(s, 1, 2, 3, 4, 5)
  clear(s)
}

في نسخة go1.21، أُضيفت الدالة المدمجة clear لمسح محتوى الشريحة، أو جعل جميع العناصر صفرية. عندما تعمل clear على شريحة، المترجم في وقت الترجمة يوسعها بواسطة cmd/compile/internal/walk.arrayClear للشكل التالي:

go
if len(s) != 0 {
  hp = &s[0]
  hn = len(s)*sizeof(elem(s))
    if elem(s).hasPointer() {
        memclrHasPointers(hp, hn)
    }else {
        memclrNoHeapPointers(hp, hn)
    }
}

أولًا التحقق من أن طول الشريحة ليس 0، ثم حساب عدد البايتات المراد مسحها، ثم المعالجة بحسب ما إذا كان العنصر مؤشرًا. النهاية تستخدم memclrNoHeapPointers:

go
func memclrNoHeapPointers(ptr unsafe.Pointer, n uintptr)

تستقبل معاملين: مؤشر لعنوان البداية، والإزاحة (عدد البايتات المراد مسحها). عنوان بداية الذاكرة هو عنوان مرجع الشريحة، الإزاحة n = sizeof(et) * len. الدالة منفذة بالتجميع.

جدير بالذكر أنه إذا حاولت استخدام اجتياز لمسح المصفوفة، مثل:

go
for i := range s {
  s[i] = ZERO_val
}

قبل وجود clear، كانت هذه الطريقة شائعة. الآن في وقت التريجة، سيُحسَّن هذا الكود بواسطة cmd/compile/internal/walk.arrayRangeClear للشكل التالي:

go
for i, v := range s {
    if len(s) != 0 {
        hp = &s[0]
        hn = len(s)*sizeof(elem(s))
        if elem(s).hasPointer() {
            memclrHasPointers(hp, hn)
        }else {
            memclrNoHeapPointers(hp, hn)
        }
        // إيقاف الحلقة
        i = len(s) - 1
    }
}

المنطق نفسه، مع إضافة i = len(s)- 1 لإيقاف الحلقة بعد مسح الذاكرة.

الاجتياز

go
for i, e := range s {
  fmt.Println(i, e)
}

عند استخدام for range لاجتياز شريحة، تُوسع بواسطة walkRange في cmd/compile/internal/walk/range.go للشكل التالي:

go
// نسخ البنية
hs := s
// الحصول على مؤشر المصفوفة الأساسية
hu = uintptr(unsafe.Pointer(hs.ptr))
v1 := 0
v2 := zero
for i := 0; i < hs.len; i++ {
    hp = (*T)(unsafe.Pointer(hu))
    v1, v2 = i, *hp
    ... body of loop ...
    hu = uintptr(unsafe.Pointer(hp)) + elemsize
}

يمكن ملاحظة أن تنفيذ for range لا يزال يجتاز العناصر بتحريك المؤشر. لتجنب تحديث الشريحة أثناء الاجتياح، نُسخت بنية مسبقًا hs. لتجنب أن يشير المؤشر لذاكرة خارج الحدود بعد انتهاء الاجتياح، hu يستخدم نوع uintptr لتخزين العنوان، وعند الحاجة للوصول للعنصر يُحوَّل لـ unsafe.Pointer.

المتغير v2 أي e في for range، خلال الاجتياح كله متغير واحد، يُغطى ولا يُعاد إنشاؤه. هذا سبب مشكلة متغير الحلقة التي حيرت مطوري Go لعشر سنوات، حتى نسخة go.1.21 قررت Go أخيرًا حلها. في التحديثات القادمة، قد يصبح إنشاء v2 كالتالي:

go
v2 := *hp

عملية بناء الكود الوسيط محذوفة هنا، لا تنتمي لمعرفة الشرائح.

Golang تم تحريره بواسطة www.golangdev.cn