Skip to content

title: Slice-Datenstruktur keywords: [Go, Datenstruktur, Slice] description: Slice ist wahrscheinlich die am häufigsten verwendete Datenstruktur in Go, ohne Ausnahme (tatsächlich gibt es nur wenige eingebaute Datenstrukturen). Es ist fast überall zu sehen. Die grundlegende Verwendung wurde bereits in der Spracheinführung erläutert. Im Folgenden wird untersucht, wie es intern aussieht und wie es intern funktioniert.

slice

TIP

Zum Lesen dieses Artikels werden Kenntnisse der Standardbibliothek unsafe benötigt.

Slice ist wahrscheinlich die am häufigsten verwendete Datenstruktur in Go, ohne Ausnahme (tatsächlich gibt es nur wenige eingebaute Datenstrukturen). Es ist fast überall zu sehen. Die grundlegende Verwendung wurde bereits in der Spracheinführung erläutert. Im Folgenden wird untersucht, wie es intern aussieht und wie es intern funktioniert.

Struktur

Die Implementierung von Slice befindet sich in der Datei runtime/slice.go. Zur Laufzeit existiert ein Slice als Struktur vom Typ runtime.slice, wie unten gezeigt.

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

Diese Struktur hat nur drei Felder:

  • array, ein Zeiger auf das zugrunde liegende Array
  • len, die Länge des Slices, bezieht sich auf die Anzahl der bereits vorhandenen Elemente im Array
  • cap, die Kapazität des Slices, bezieht sich auf die Gesamtzahl der Elemente, die das Array aufnehmen kann

Aus den oben genannten Informationen geht hervor, dass die zugrunde liegende Implementierung von Slices immer noch auf Arrays basiert. Normalerweise ist es nur eine Struktur, die nur eine Referenz auf das Array sowie Aufzeichnungen über Kapazität und Länge hält. Dadurch werden die Kosten für die Übergabe eines Slices sehr gering - es muss nur die Referenz auf die Daten kopiert werden, nicht alle Daten. Bei der Verwendung von len und cap zum Abrufen der Länge und Kapazität eines Slices wird lediglich auf dessen Feldwerte zugegriffen, ohne das Array durchlaufen zu müssen.

Dies bringt jedoch auch einige schwer erkennbare Probleme mit sich. Betrachten wir das folgende Beispiel:

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]

Im obigen Code wurde s1 durch Slicing erstellt, aber es und das ursprüngliche Slice referenzieren dasselbe zugrunde liegende Array. Das Ändern der Daten in s1 führt auch zu Änderungen in s. Beim Kopieren von Slices sollte daher die copy-Funktion verwendet werden, die kopierten Slices haben nichts mit dem ursprünglichen zu tun. Betrachten wir ein weiteres Beispiel:

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]

Ebenfalls wurde das Slice durch Slicing kopiert, aber dieses Mal hat es keine Auswirkungen auf das ursprüngliche Slice. Zunächst zeigten s1 und s tatsächlich auf dasselbe Array, aber nachdem s1 zu viele Elemente hinzugefügt wurden, die die Kapazität des Arrays überschritten, wurde ein größeres neues Array zugewiesen, um die Elemente aufzunehmen. Am Ende zeigten sie auf unterschiedliche Arrays. Denken Sie, dass das Problem jetzt gelöst ist? Dann betrachten wir ein weiteres Beispiel:

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...)
}
[]

Obwohl Elemente hinzugefügt wurden, wird ein leeres Slice ausgegeben. Tatsächlich wurden die Daten dem Slice hinzugefügt, aber sie wurden in das zugrunde liegende Array geschrieben. In Go werden Funktionsparameter als Wert übergeben, daher ist der Parameter s tatsächlich eine Kopie der ursprünglichen Slice-Struktur. Die append-Operation gibt nach dem Hinzufügen von Elementen eine Slice-Struktur mit aktualisierter Länge zurück, aber zugewiesen wird nur dem Parameter s, nicht dem ursprünglichen Slice s - es besteht keine Verbindung zwischen beiden.

Für ein Slice hängen die Startposition, auf die es zugreifen und die es ändern kann, von der Referenzposition auf das Array ab, und der Offset hängt von der in der Struktur aufgezeichneten Länge ab. Der Zeiger in der Struktur kann nicht nur auf den Anfang, sondern auch auf die Mitte des Arrays zeigen, wie in der folgenden Abbildung gezeigt.

Ein zugrunde liegendes Array kann von vielen Slices referenziert werden, wobei die Referenzposition und der Bereich unterschiedlich sein können, wie in der obigen Abbildung. Diese Situation tritt normalerweise beim Slicing von Slices auf, ähnlich dem folgenden Code:

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

Beim Slicing entspricht die Kapazität des neu erstellten Slices der Array-Länge abzüglich der Startposition, auf die der neue Slice verweist. Zum Beispiel hat der durch s[4:6] erzeugte neue Slice eine Kapazität von 6 = 10 - 4. Natürlich müssen die Bereiche, auf die Slices verweisen, nicht benachbart sein - sie können sich auch überschneiden. Dies kann jedoch erhebliche Probleme verursachen, da die Daten des aktuellen Slices ohne Wissen von einem anderen Slice geändert werden können. Wie der violette Slice in der obigen Abbildung zeigt: Wenn im weiteren Verlauf Elemente mit append hinzugefügt werden, könnten die Daten des grünen und blauen Slices überschrieben werden. Um dies zu vermeiden, erlaubt Go, beim Slicing den Kapazitätsbereich festzulegen. Die Syntax lautet wie folgt:

go
s4 = s[4:6:6]

In diesem Fall wird die Kapazität auf 2 beschränkt, so dass das Hinzufügen von Elementen eine Erweiterung auslöst. Nach der Erweiterung entsteht ein neues Array, das keine Beziehung zum ursprünglichen Array mehr hat und somit keine Auswirkungen hat. Denken Sie, dass die Probleme mit Slices hier zu Ende sind? Mitnichten, betrachten wir ein weiteres Beispiel.

go
package main

import "fmt"

func main() {
  s := make([]int, 0, 10)
  // Die Anzahl der hinzugefügten Elemente übersteigt genau die Kapazität
  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...)
}
[]

Der Code unterscheidet sich nicht vom vorherigen Beispiel, es wurden nur die Parameter geändert, sodass die Anzahl der hinzugefügten Elemente gerade die Kapazität des Slices überschreitet. Dadurch wird beim Hinzufügen eine Erweiterung ausgelöst. Die Daten wurden nicht nur nicht zum ursprünglichen Slice s hinzugefügt, sondern wurden nicht einmal in das zugrunde liegende Array geschrieben, auf das es verweist. Wir können dies mit einem unsafe-Zeiger verifizieren, wie im folgenden Code gezeigt:

go
package main

import (
  "fmt"
  "unsafe"
)

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

  // Die Anzahl der hinzugefügten Elemente übersteigt genau die Kapazität
  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,

Es ist zu erkennen, dass das zugrunde liegende Array des ursprünglichen Slices vollständig leer ist - keine Daten vorhanden. Alle Daten wurden in das neue Array geschrieben, aber dies hat nichts mit dem ursprünglichen Slice zu tun. Selbst wenn append eine neue Referenz zurückgibt, wird nur der Wert des Formalparameters s geändert, was keinen Einfluss auf das ursprüngliche Slice s hat. Dass Slices als Strukturen implementiert sind, macht sie zwar sehr leichtgewichtig, aber die oben genannten Probleme sollten nicht ignoriert werden, insbesondere da sie in echtem Code oft tief verborgen sind und schwer zu entdecken sind.

Erstellung

Zur Laufzeit wird die Erstellung von Slices mit der make-Funktion von runtime.makeslice erledigt. Die Logik ist recht einfach, die Funktionssignatur lautet:

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

Sie empfängt drei Parameter: Elementtyp, Länge und Kapazität. Nach Abschluss gibt sie einen Zeiger auf das zugrunde liegende Array zurück. Der Code lautet wie folgt:

go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
    // Berechnung des benötigten Gesamtspeichers, zu große Werte führen zu Überlauf
    // 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()
  }

    // Wenn keine Probleme vorliegen, Speicher zuweisen
  return mallocgc(mem, et, true)
}

Die Logik ist sehr einfach, es werden insgesamt nur zwei Dinge getan:

  • Berechnung des benötigten Speichers
  • Zuweisung des Speicherplatzes

Wenn die Bedingungsprüfung fehlschlägt, wird direkt panic ausgelöst:

  • Numerischer Überlauf bei der Speicherberechnung
  • Das Ergebnis überschreitet den maximal zuweisbaren Speicher
  • Länge und Kapazität sind ungültig

Wenn der berechnete Speicher größer als 32KB ist, wird er auf dem Heap zugewiesen. Danach wird ein Zeiger auf das zugrunde liegende Array zurückgegeben. Der Aufbau der runtime.slice-Struktur wird nicht von der makeslice-Funktion erledigt. Tatsächlich erfolgt der Aufbau der Struktur während der Kompilierung. Die makeslice-Funktion zur Laufzeit ist nur für die Speicherzuweisung verantwortlich, ähnlich dem folgenden Code:

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

Wer interessiert ist, kann sich den generierten Zwischencode ansehen, dieser ist ähnlich.

go
name s.ptr[*int]: v11
name s.len[int]: v7
name s.cap[int]: v8

Wenn man ein Array verwendet, um einen Slice zu erstellen, wie im folgenden Beispiel:

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

Dieser Prozess ähnelt dem folgenden Code:

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

Go verwendet das Array direkt als zugrunde liegendes Array für den Slice, daher beeinflussen Änderungen an den Daten im Slice auch die Daten des Arrays. Bei der Erstellung eines Slices aus einem Array entspricht die Länge high-low und die Kapazität max-low, wobei max standardmäßig der Array-Länge entspricht. Alternativ kann die Kapazität beim Slicing manuell angegeben werden, zum Beispiel:

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

Zugriff

Der Zugriff auf Slices erfolgt wie bei Arrays über Index-Indizierung:

go
elem := s[i]

Die Zugriff-Operation auf Slices wird bereits während der Kompilierung abgeschlossen, indem Zwischencode generiert wird. Der finale Code kann als folgender Pseudocode verstanden werden:

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

Tatsächlich erfolgt der Zugriff auf das Element mit dem entsprechenden Index durch Zeiger-Verschiebung. Dies entspricht dem folgenden Teil des Codes in der Funktion cmd/compile/internal/ssagen.exprCheckPtr:

go
case ir.OINDEX:
    n := n.(*ir.IndexExpr)
    switch {
    case n.X.Type().IsSlice():
        // Zeiger versetzen
        p := s.addr(n)
        return s.load(n.X.Type().Elem(), p)

Beim Zugriff auf die Länge und Kapazität eines Slices über die Funktionen len und cap gilt derselbe Grundsatz. Dies entspricht ebenfalls einem Teil des Codes in der Funktion 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))

Im tatsächlich generierten Code wird durch Zeiger-Verschiebung auf das len-Feld der Slice-Struktur zugegriffen. Dies kann als folgender Pseudocode verstanden werden:

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

Angenommen, wir haben folgenden Code:

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

Dann sieht der Zwischencode in einer bestimmten Phase der Generierung wahrscheinlich so aus:

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

Aus dem Obigen lässt sich grob erkennen: einer plus 8, einer plus 16 - offensichtlich werden die Slice-Felder durch Zeiger-Verschiebung zugegriffen.

Wenn Länge und Kapazität bereits während der Kompilierung abgeleitet werden können, müssen zur Laufzeit keine Zeiger verschoben werden, um die Werte zu erhalten. In folgendem Fall ist beispielsweise keine Zeiger-Verschiebung erforderlich:

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

Die Werte der Variablen l und c werden direkt durch 10 und 20 ersetzt.

Schreiben

Ändern

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

Beim Ändern von Slice-Werten über Index-Indizierung wird während der Kompilierung durch die OpStore-Operation ähnlicher Pseudocode generiert:

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

Der Zwischencode in einer bestimmten Phase der Generierung sieht wahrscheinlich so aus:

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]:

Es ist zu erkennen, dass der Code auf die Slice-Länge zugreift, um zu prüfen, ob der Index gültig ist, und schließlich durch Zeiger-Verschiebung Elemente speichert.

Hinzufügen

Mit der append-Funktion können Elemente zu einem Slice hinzugefügt werden:

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

Nach dem Hinzufügen von Elementen gibt es eine neue Slice-Struktur zurück. Wenn keine Erweiterung stattfindet, wird nur die Länge im Vergleich zum ursprünglichen Slice aktualisiert; andernfalls verweist es auf ein neues Array. Die Verwendungsprobleme von append wurden im Abschnitt Struktur bereits ausführlich erläutert und werden nicht weiter vertieft. Im Folgenden wird untersucht, wie append funktioniert.

Zur Laufzeit gibt es keine entsprechende Funktion wie runtime.appendslice. Das Hinzufügen von Elementen wird tatsächlich bereits während der Kompilierung erledigt. Die append-Funktion wird in den entsprechenden Zwischencode expandiert. Der Code für die Entscheidung befindet sich in der Funktion 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)
    }

Es ist zu erkennen, dass in drei Fälle unterschieden wird:

  • Hinzufügen mehrerer Elemente
  • Hinzufügen eines Slices
  • Hinzufügen eines temporär erstellten Slices

Im Folgenden wird erläutert, wie der generierte Code aussieht, um zu verstehen, wie append tatsächlich funktioniert. Wer sich für den Prozess der Codegenerierung interessiert, kann sich selbst weiter informieren.

Elemente hinzufügen

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

Wenn nur eine begrenzte Anzahl von Elementen hinzugefügt wird, wird dies von der walkAppend-Funktion in folgenden Code expandiert:

go
// Anzahl der hinzuzufügenden Elemente
const argc = len(args) - 1
newLen := s.len + argc

// Ob eine Erweiterung erforderlich ist
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

Zuerst wird die Anzahl der hinzuzufügenden Elemente berechnet, dann wird geprüft, ob eine Erweiterung erforderlich ist, und schließlich werden die Elemente einzeln zugewiesen.

Slice hinzufügen

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

Wenn ein Slice direkt hinzugefügt wird, wird dies von der appendSlice-Funktion in folgenden Code expandiert:

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))

Wie zuvor wird die neue Länge berechnet und geprüft, ob eine Erweiterung erforderlich ist. Der Unterschied ist, dass Go nicht jedes Element des Quell-Slices einzeln hinzufügt, sondern den Speicher direkt kopiert.

Temporären Slice hinzufügen

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

Wenn ein temporär erstellter Slice hinzugefügt wird, wird dies von der extendslice-Funktion in folgenden Code expandiert:

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)

Bei einem temporär hinzugefügten Slice ermittelt Go die Länge des temporären Slices. Wenn die Kapazität des aktuellen Slices nicht ausreicht, wird eine Erweiterung versucht. Anschließend wird der entsprechende Speicherbereich gelöscht.

Erweiterung

Aus dem Abschnitt zur Struktur geht hervor, dass die zugrunde liegende Implementierung eines Slices immer noch ein Array ist. Ein Array ist eine Datenstruktur mit fester Länge, aber die Länge eines Slices ist variabel. Wenn die Kapazität des Arrays nicht ausreicht, fordert der Slice einen größeren Speicherbereich an, um die Daten zu speichern - also ein neues Array. Anschließend werden die alten Daten dorthin kopiert und die Referenz des Slices verweist auf das neue Array. Dieser Prozess wird als Erweiterung (Expansion) bezeichnet. Die Erweiterung wird zur Laufzeit von der Funktion runtime.growslice durchgeführt. Die Funktionssignatur lautet:

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

Kurze Erklärung der Parameter:

  • oldPtr, Zeiger auf das alte Array
  • newLen, Länge des neuen Arrays, newLen = oldLen + num
  • oldCap, Kapazität des alten Slices, entspricht der Länge des alten Arrays
  • et, Elementtyp

Der Rückgabewert ist ein neuer Slice. Der neue Slice hat nichts mit dem ursprünglichen Slice zu tun, der einzige Gemeinsamkeit ist, dass die gespeicherten Daten identisch sind.

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

Beim Hinzufügen von Elementen mit append muss der Rückgabewert den ursprünglichen Slice überschreiben. Wenn eine Erweiterung stattfindet, wird ein neuer Slice zurückgegeben.

Bei der Erweiterung muss zunächst die neue Länge und Kapazität bestimmt werden, entsprechend dem folgenden Code:

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
// Doppelte Kapazität
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
        }
        // Numerischer Überlauf
        if newcap <= 0 {
            newcap = newLen
        }
    }
}

Aus dem oben genannten Code geht hervor, dass bei Slices mit einer Kapazität kleiner als 256 die Kapazität verdoppelt wird. Bei Slices mit einer Kapazität größer oder gleich 256 beträgt die neue Kapazität mindestens das 1,25-fache der ursprünglichen Kapazität. Wenn der aktuelle Slice klein ist, wird er jedes Mal direkt verdoppelt, um häufige Erweiterungen zu vermeiden. Wenn der Slice größer ist, wird der Erweiterungsfaktor reduziert, um übermäßige Speicheranforderungen und Verschwendung zu vermeiden.

Nachdem die neue Länge und Kapazität ermittelt wurden, wird der benötigte Speicher berechnet, entsprechend dem folgenden Code:

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)
    // Endgültige Kapazität
    newcap = int(capmem / et.Size_)
    capmem = uintptr(newcap) * et.Size_
}

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

Die Speicherberechnungsformel lautet mem = cap * sizeof(et). Zur Erleichterung der Speicherausrichtung wird der berechnete Speicher während des Prozesses auf die nächste Zweierpotenz aufgerundet und die neue Kapazität erneut berechnet. Wenn die neue Kapazität zu groß ist und ein numerischer Überlauf auftritt, oder wenn der neue Speicher den maximal zuweisbaren Speicher überschreitet, wird panic ausgelöst.

go
var p unsafe.Pointer
// Speicher zuweisen
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)

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

Nachdem die erforderlichen Ergebnisse berechnet wurden, wird Speicher der angegebenen Größe zugewiesen, dann der Speicherbereich von newLen bis newCap gelöscht, anschließend die Daten des alten Arrays in den neuen Slice kopiert und schließlich die Slice-Struktur erstellt.

Kopieren

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

Beim Kopieren von Slices mit der copy-Funktion entscheidet der während der Kompilierung von cmd/compile/internal/walk.walkcopy generierte Code, auf welche Weise kopiert wird. Wenn der Aufruf zur Laufzeit erfolgt, wird die Funktion runtime.slicecopy verwendet, die für das Kopieren von Slices verantwortlich ist. Die Funktionssignatur lautet:

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

Sie empfängt die Zeiger und Längen des Quell- und Zielslices sowie die zu kopierende Länge width. Die Logik dieser Funktion ist sehr einfach, wie unten gezeigt:

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
  }

  // Berechnung der zu kopierenden Bytes
  size := uintptr(n) * width

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

Der Wert von width hängt vom kleineren der beiden Slice-Längen ab. Es ist zu erkennen, dass beim Kopieren von Slices nicht jedes Element einzeln durchlaufen wird, sondern der Speicher des zugrunde liegenden Arrays als Ganzes kopiert wird. Bei großen Slices kann das Kopieren des Speichers erhebliche Auswirkungen auf die Leistung haben.

Wenn der Aufruf nicht zur Laufzeit erfolgt, wird der Code in folgende Form expandiert:

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)))
}

Das Prinzip beider Methoden ist identisch: Slices werden durch Kopieren des Speichers kopiert. Die memmove-Funktion ist in Assembler implementiert. Wer interessiert ist, kann die Details in runtime/memmove_amd64.s einsehen.

Leeren

go
package main

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

In der Version go1.21 wurde die eingebaute Funktion clear hinzugefügt, die zum Leeren des Inhalts eines Slices verwendet werden kann, oder genauer gesagt, um alle Elemente auf ihren Nullwert zu setzen. Wenn die clear-Funktion auf einen Slice angewendet wird, wird sie vom Compiler während der Kompilierung von der Funktion cmd/compile/internal/walk.arrayClear in folgende Form expandiert:

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)
    }
}

Zunächst wird geprüft, ob die Slice-Länge 0 ist, dann wird die Anzahl der zu bereinigenden Bytes berechnet, und abhängig davon, ob die Elemente Zeiger sind, werden zwei Fälle unterschieden. Schließlich wird jedoch immer die Funktion memclrNoHeapPointers verwendet, deren Signatur wie folgt lautet:

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

Sie empfängt zwei Parameter: einen Zeiger auf die Startadresse und den Offset, also die Anzahl der zu bereinigenden Bytes. Die Startadresse des Speichers ist die Adresse der Referenz, die der Slice hält, und der Offset ist n = sizeof(et) * len. Diese Funktion ist in Assembler implementiert. Wer interessiert ist, kann die Details in runtime/memclr_amd64.s einsehen.

Erwähnenswert ist, dass wenn im Quellcode versucht wird, ein Array durch Iteration zu leeren, wie zum Beispiel:

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

Vor der Einführung der clear-Funktion wurde ein Slice normalerweise auf diese Weise geleert. Zur Kompilierungszeit wird dieser Code jetzt von der Funktion cmd/compile/internal/walk.arrayRangeClear in folgende Form optimiert:

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)
        }
        // Schleife beenden
        i = len(s) - 1
    }
}

Die Logik ist genau dieselbe wie oben, mit einer zusätzlichen Zeile i = len(s)-1, die dazu dient, die Schleife nach dem Löschen des Speichers zu beenden.

Iteration

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

Bei der Verwendung von for range zum Durchlaufen eines Slices wird der Code von der walkRange-Funktion in cmd/compile/internal/walk/range.go in folgende Form expandiert:

go
// Struktur kopieren
hs := s
// Zeiger auf das zugrunde liegende Array abrufen
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
}

Es ist zu erkennen, dass die Implementierung von for range weiterhin durch Zeiger-Verschiebung die Elemente durchläuft. Um zu vermeiden, dass der Slice während der Iteration aktualisiert wird, wird vorab eine Kopie der Struktur hs erstellt. Um zu verhindern, dass der Zeiger nach Ende der Iteration auf Speicher außerhalb der Grenzen verweist, verwendet hu den Typ uintptr zum Speichern der Adresse und wird erst bei Bedarf in unsafe.Pointer konvertiert.

Die Variable v2, also e in for range, ist während des gesamten Iterationsprozesses von Anfang bis Ende dieselbe Variable. Sie wird nur überschrieben, nicht neu erstellt. Dies führte zum berüchtigten Schleifenvariablen-Problem, das Go-Entwickler zehn Jahre lang beschäftigte. Erst in Version go.1.21 entschied sich das offizielle Team, dieses Problem zu lösen. In zukünftigen Versionsupdates könnte die Erstellung von v2 wie folgt aussehen:

go
v2 := *hp

Der Prozess der Konstruktion des Zwischencodes wird hier ausgelassen, da er nicht zum Wissensbereich von Slices gehört. Wer interessiert ist, kann sich selbst weiter informieren.

Golang by www.golangdev.cn edit