Skip to content

slice

TIP

Membaca artikel ini memerlukan pengetahuan tentang pustaka standar unsafe.

Slice mungkin adalah struktur data yang paling umum digunakan dalam bahasa Go, tidak ada yang lain (sebenarnya tidak ada banyak struktur data built-in), hampir di mana-mana dapat dilihat kehadirannya. Tentang penggunaan dasarnya telah dijelaskan dalam pengenalan bahasa, di bawah ini mari lihat seperti apa bentuk internalnya, dan bagaimana cara kerjanya di internal.

Struktur

Mengenai implementasi slice, kode sumbernya terletak di file runtime/slice.go. Pada runtime, slice ada sebagai struktur, tipenya adalah runtime.slice, seperti berikut.

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

Struktur ini hanya memiliki tiga field

  • array, pointer yang menunjuk ke array tingkat rendah
  • len, panjang slice, mengacu pada jumlah elemen yang ada di array
  • cap, kapasitas slice, mengacu pada total jumlah elemen yang dapat ditampung array

Dari informasi di atas dapat diketahui bahwa implementasi tingkat rendah slice masih bergantung pada array, biasanya hanya struktur yang hanya memegang referensi ke array, dan catatan kapasitas dan panjang. Dengan demikian biaya传递 slice akan sangat rendah, hanya perlu menyalin referensi datanya, tidak perlu menyalin semua data, dan saat menggunakan len dan cap untuk mendapatkan panjang dan kapasitas slice, sama seperti mendapatkan nilai field-nya, tidak perlu traversing array.

Namun ini juga akan membawa beberapa masalah yang tidak mudah ditemukan, lihat contoh berikut

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]

Dalam kode di atas, s1 membuat slice baru melalui cara pemotongan, tetapi ia dan slice sumber sama-sama mengacu pada array tingkat rendah yang sama, memodifikasi data di s1 juga akan menyebabkan s berubah. Jadi saat menyalin slice seharusnya menggunakan fungsi copy, slice yang disalin oleh yang terakhir tidak ada hubungannya dengan yang sebelumnya. Mari lihat contoh lain

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]

Masih menggunakan cara pemotongan untuk menyalin slice, tetapi kali ini tidak akan mempengaruhi slice sumber. Awalnya s1 dan s memang指向 array yang sama, tetapi kemudian menambahkan terlalu banyak elemen ke s1 melebihi jumlah yang dapat ditampung array, sehingga mengalokasikan array baru yang lebih besar untuk menampung elemen, jadi akhirnya mereka berdua指向 array yang berbeda. Apakah merasa sudah tidak ada masalah, mari lihat contoh lain

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

Jelas sudah menambahkan elemen, tetapi yang dicetak adalah slice kosong, sebenarnya data memang sudah ditambahkan ke slice, hanya ditulis ke array tingkat rendah. Parameter fungsi di Go adalah传递 nilai, jadi parameter s sebenarnya adalah salinan dari struktur slice sumber, dan operasi append akan mengembalikan struktur slice yang diperbarui panjangnya setelah menambahkan elemen, hanya yang diassign adalah parameter s bukan slice sumber s, keduanya sebenarnya tidak ada hubungan.

Untuk sebuah slice, posisi awal yang dapat diakses dan dimodifikasi tergantung pada posisi referensi array, offset tergantung pada panjang yang dicatat dalam struktur. Pointer dalam struktur selain dapat menunjuk ke awal, juga dapat ke tengah array, seperti gambar berikut.

Sebuah array tingkat rendah dapat dirujuk oleh banyak slice, dan posisi dan rentang referensi dapat berbeda, seperti gambar di atas, situasi ini umumnya muncul saat melakukan pemotongan slice, mirip dengan kode berikut

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

Saat memotong, kapasitas slice baru yang dihasilkan sama dengan panjang array dikurangi posisi awal yang dirujuk slice baru. Misalnya kapasitas slice baru yang dihasilkan s[4:6] adalah 6 = 10 - 4. Tentu saja, rentang referensi slice juga tidak harus berdekatan, dapat saling bersilangan, tetapi ini akan menghasilkan masalah yang sangat besar, mungkin data slice saat ini tanpa sepengetahuan dimodifikasi oleh slice lain, seperti slice ungu dalam gambar di atas, jika menggunakan append untuk menambahkan elemen dalam proses selanjutnya, mungkin akan menimpa data slice hijau dan biru. Untuk menghindari situasi ini, Go mengizinkan pengaturan rentang kapasitas saat memotong, sintaksnya adalah sebagai berikut.

go
s4 = s[4:6:6]

Dalam kasus ini, kapasitasnya dibatasi hingga 2, maka menambahkan elemen akan memicu ekspansi, setelah ekspansi adalah array baru, tidak ada hubungan dengan array sumber, tidak akan ada pengaruh. Apakah mengira masalah tentang slice berakhir di sini, sebenarnya tidak, mari lihat contoh lain.

go
package main

import "fmt"

func main() {
  s := make([]int, 0, 10)
  // Jumlah elemen yang ditambahkan tepat lebih besar dari kapasitas
  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...)
}
[]

Kode tidak berbeda dengan contoh sebelumnya, hanya mengubah parameter input, membuat jumlah elemen yang ditambahkan tepat lebih besar dari kapasitas slice, sehingga akan memicu ekspansi saat menambahkan, dengan demikian data tidak hanya tidak ditambahkan ke slice sumber s, bahkan array tingkat rendah yang ditunjuknya juga tidak ditulis data, kita dapat mengkonfirmasi ini melalui pointer unsafe, kodenya adalah sebagai berikut

go
package main

import (
  "fmt"
  "unsafe"
)

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

  // Jumlah elemen yang ditambahkan tepat lebih besar dari kapasitas
  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,

Dapat dilihat bahwa array tingkat rendah slice sumber kosong, tidak ada apa-apa, data semua ditulis ke array baru, tetapi tidak ada hubungan dengan slice sumber, karena bahkan append mengembalikan referensi baru, yang dimodifikasi hanya nilai parameter s, tidak mempengaruhi slice sumber s. Slice sebagai struktur memang dapat membuatnya sangat ringan, tetapi masalah di atas juga tidak dapat diabaikan, terutama dalam kode aktual masalah ini biasanya sangat tersembunyi, sulit ditemukan.

Pembuatan

Pada runtime, pekerjaan membuat slice dengan fungsi make diselesaikan oleh runtime.makeslice, logikanya cukup sederhana, signature fungsi ini adalah sebagai berikut

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

Ia menerima tiga parameter, tipe elemen, panjang, kapasitas, setelah selesai mengembalikan pointer yang menunjuk ke array tingkat rendah, kodenya adalah sebagai berikut

go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
    // Hitung total memori yang dibutuhkan, jika terlalu besar akan menyebabkan overflow numerik
    // 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()
  }

    // Jika tidak ada masalah, alokasikan memori
  return mallocgc(mem, et, true)
}

Dapat dilihat logikanya sangat sederhana, total hanya melakukan dua hal

  • Menghitung memori yang dibutuhkan
  • Mengalokasikan ruang memori

Jika pemeriksaan kondisi gagal, akan langsung panic

  • Nilai overflow saat menghitung memori
  • Hasil perhitungan lebih besar dari memori maksimum yang dapat dialokasikan
  • Panjang dan kapasitas tidak合法

Jika memori yang dihitung lebih besar dari 32KB, akan mengalokasikannya ke heap, setelah selesai akan mengembalikan pointer yang menunjuk ke array tingkat rendah, pekerjaan membangun struktur runtime.slice tidak diselesaikan oleh fungsi makeslice. Sebenarnya, pekerjaan membangun struktur diselesaikan selama kompilasi, fungsi makeslice runtime hanya bertanggung jawab untuk mengalokasikan memori, mirip dengan kode berikut.

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

Jika tertarik dapat melihat kode perantara yang dihasilkan, mirip dengan ini.

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

Jika menggunakan array untuk membuat slice, seperti berikut

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

Proses ini mirip dengan kode berikut

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

Go akan langsung menggunakan array tersebut sebagai array tingkat rendah slice, jadi memodifikasi data di slice juga akan mempengaruhi data array. Saat menggunakan array untuk membuat slice, panjang sama dengan high-low, kapasitas sama dengan max-low, di mana max default adalah panjang array, atau juga dapat menentukan kapasitas secara manual saat memotong, misalnya.

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

Akses

Mengakses slice sama seperti mengakses array menggunakan indeks subscript

go
elem := s[i]

Operasi akses slice sudah diselesaikan selama kompilasi, melalui cara menghasilkan kode perantara untuk mengakses, kode akhir yang dihasilkan dapat dipahami sebagai kode pseudo berikut

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

Sebenarnya adalah melalui operasi memindahkan pointer untuk mengakses elemen subscript yang sesuai, sesuai dengan bagian kode berikut di fungsi cmd/compile/internal/ssagen.exprCheckPtr

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

Saat mengakses panjang dan kapasitas slice melalui fungsi len dan cap, juga sama, juga sesuai dengan bagian kode berikut di fungsi 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))

Dalam kode yang sebenarnya dihasilkan, melalui memindahkan pointer untuk mengakses field len dalam struktur slice, dapat dipahami sebagai kode pseudo berikut

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

Misalkan sekarang ada kode berikut

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

Maka di beberapa tahap kode perantara yang dihasilkan kemungkinan besar seperti ini

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

Dari atas kira-kira dapat dilihat, satu tambah 8, satu tambah 16, jelas adalah melalui offset pointer untuk mengakses field slice.

Jika dapat menyimpulkan panjang dan kapasitasnya selama kompilasi, tidak akan mendapatkan nilai dengan offset pointer pada runtime, seperti situasi berikut tidak perlu memindahkan pointer.

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

Nilai variabel l dan c akan langsung diganti menjadi 10 dan 20.

Modifikasi

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

Saat memodifikasi nilai slice melalui indeks subscript, selama kompilasi akan menghasilkan kode pseudo berikut melalui operasi OpStore

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

Di beberapa tahap kode perantara yang dihasilkan kemungkinan besar seperti ini

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

Dapat dilihat kode mengakses panjang slice untuk memeriksa apakah subscript合法, akhirnya melalui memindahkan pointer untuk menyimpan elemen.

Menambahkan

Melalui fungsi append dapat menambahkan elemen ke slice

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

Setelah menambahkan elemen, ia akan mengembalikan struktur slice baru, jika tidak ada ekspansi dibandingkan dengan slice sumber hanya memperbarui panjang, jika tidak akan menunjuk ke array baru. Tentang masalah penggunaan append di bagian Struktur sudah dijelaskan dengan sangat detail, tidak akan dijelaskan lebih lanjut di sini, di bawah ini akan fokus pada bagaimana cara kerja append.

Pada runtime, tidak ada fungsi seperti runtime.appendslice yang sesuai, pekerjaan menambahkan elemen sebenarnya sudah dilakukan selama kompilasi, fungsi append akan展开 menjadi kode perantara yang sesuai, kode判断 terletak di fungsi 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)
    }

Dapat dilihat dibagi menjadi tiga situasi

  • Menambahkan beberapa elemen
  • Menambahkan sebuah slice
  • Menambahkan slice yang dibuat sementara

Di bawah ini akan menjelaskan seperti apa kode yang dihasilkan, sehingga memahami bagaimana sebenarnya cara kerja append, jika tertarik dengan proses pembuatan kode dapat memahaminya sendiri.

Menambahkan elemen

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

Jika hanya menambahkan sejumlah elemen terbatas, akan展开 oleh fungsi walkAppend menjadi kode berikut

go
// Jumlah elemen yang akan ditambahkan
const argc = len(args) - 1
newLen := s.len + argc

// Apakah perlu ekspansi
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

Pertama menghitung jumlah elemen yang akan ditambahkan, kemudian判断 apakah perlu ekspansi, terakhir satu per satu assign.

Menambahkan slice

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

Jika langsung menambahkan sebuah slice, akan展开 oleh fungsi appendSlice menjadi kode berikut

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

Masih sama seperti sebelumnya, menghitung panjang baru,判断 apakah perlu ekspansi, yang berbeda adalah Go tidak akan menambahkan elemen slice sumber satu per satu, tetapi memilih untuk langsung menyalin memori.

Menambahkan slice sementara

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

Jika menambahkan slice yang dibuat sementara, akan展开 oleh fungsi extendslice menjadi kode berikut

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)

Untuk slice yang ditambahkan sementara, Go akan mendapatkan panjang slice sementara, jika kapasitas slice saat ini tidak cukup untuk menampung, akan mencoba ekspansi, setelah selesai juga akan menghapus bagian memori yang sesuai.

Ekspansi

Dari bagian struktur dapat diketahui bahwa tingkat rendah slice masih merupakan array, array adalah struktur data dengan panjang tetap, tetapi panjang slice dapat berubah. Saat kapasitas array tidak cukup, slice akan meminta ruang memori yang lebih besar untuk menyimpan data, yaitu array baru, kemudian menyalin data lama ke sana, kemudian referensi slice akan menunjuk ke array baru, proses ini disebut ekspansi. Pekerjaan ekspansi diselesaikan pada runtime oleh fungsi runtime.growslice, signature fungsinya adalah sebagai berikut

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

Penjelasan singkat parameter

  • oldPtr, pointer yang menunjuk ke array lama
  • newLen, panjang array baru, newLen = oldLen + num
  • oldCap, kapasitas slice lama, sama dengan panjang array lama
  • et, tipe elemen

Nilai return-nya mengembalikan slice baru, slice baru tidak ada hubungannya dengan slice asli, satu-satunya kesamaan adalah data yang disimpan sama.

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

Saat menggunakan append untuk menambahkan elemen, akan meminta nilai return-nya menimpa slice asli, jika terjadi ekspansi, yang dikembalikan adalah slice baru.

Saat ekspansi, pertama perlu menentukan panjang dan kapasitas baru, sesuai dengan kode berikut

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
// Kapasitas ganda
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
        }
        // Overflow numerik
        if newcap <= 0 {
            newcap = newLen
        }
    }
}

Dari kode di atas dapat diketahui, untuk slice dengan kapasitas kurang dari 256, kapasitas bertambah dua kali lipat, sedangkan untuk slice dengan kapasitas lebih besar atau sama dengan 256, setidaknya akan 1,25 kali dari kapasitas asli, saat slice lebih kecil, setiap kali langsung bertambah dua kali lipat, dapat menghindari ekspansi yang sering, saat slice lebih besar, rasio ekspansi akan berkurang, menghindari申请 terlalu banyak memori yang menyebabkan pemborosan.

Setelah mendapatkan panjang dan kapasitas baru, kemudian menghitung memori yang dibutuhkan, sesuai dengan kode berikut

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)
    // Kapasitas akhir
    newcap = int(capmem / et.Size_)
    capmem = uintptr(newcap) * et.Size_
}

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

Rumus perhitungan memori adalah mem = cap * sizeof(et), untuk memudahkan penyelarasan memori, selama proses akan membulatkan memori yang dihitung ke atas menjadi kelipatan 2, dan menghitung kapasitas baru lagi. Jika kapasitas baru terlalu besar menyebabkan overflow numerik saat perhitungan, atau memori baru melebihi memori maksimum yang dapat dialokasikan, akan panic.

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

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

Setelah menghitung hasil yang dibutuhkan, alokasikan memori dengan ukuran yang ditentukan, kemudian hapus memori di rentang newLen hingga newCap, kemudian salin data array lama ke slice baru, terakhir bangun struktur slice.

Penyalinan

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

Saat menggunakan fungsi copy untuk menyalin slice, akan ditentukan oleh kode yang dihasilkan oleh cmd/compile/internal/walk.walkcopy selama kompilasi untuk menyalin dengan cara apa, jika dipanggil pada runtime, akan menggunakan fungsi runtime.slicecopy, fungsi ini bertanggung jawab untuk menyalin slice, signature fungsinya adalah sebagai berikut

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

Ia menerima pointer dan panjang slice sumber dan tujuan, serta panjang yang akan disalin width. Logika fungsi ini sangat sederhana, seperti berikut

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
  }

  // Hitung jumlah byte yang akan disalin
  size := uintptr(n) * width

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

Nilai width, tergantung pada nilai minimum panjang dua slice. Dapat dilihat bahwa saat menyalin slice tidak traversing elemen satu per satu untuk menyalin, tetapi memilih untuk langsung menyalin seluruh memori array tingkat rendah, saat slice sangat besar拷贝 memori membawa dampak performa yang tidak kecil.

Jika tidak dipanggil pada runtime, akan展开 menjadi kode dalam bentuk berikut

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

Prinsip kedua cara sama, keduanya menyalin slice melalui cara menyalin memori. Fungsi memmove diimplementasikan oleh assembly, jika tertarik dapat melihat detailnya di runtime/memmove_amd64.s.

Pengosongan

go
package main

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

Dalam versi go1.21, ditambahkan fungsi built-in clear函数 dapat digunakan untuk mengosongkan konten slice, atau mengatakan menempatkan semua elemen menjadi nilai nol. Saat fungsi clear bekerja pada slice, compiler akan展开 menjadi bentuk berikut oleh fungsi cmd/compile/internal/walk.arrayClear selama kompilasi

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

Pertama判断 apakah panjang slice 0, kemudian menghitung byte yang perlu dibersihkan, kemudian berdasarkan apakah elemen adalah pointer dibagi menjadi dua situasi untuk menangani, tetapi akhirnya akan menggunakan fungsi memclrNoHeapPointers, signature-nya adalah sebagai berikut.

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

Ia menerima dua parameter, satu adalah pointer yang menunjuk ke alamat awal, yang lain adalah offset, yaitu byte yang akan dibersihkan. Alamat awal memori adalah alamat referensi yang dipegang slice, offset n = sizeof(et) * len, fungsi ini diimplementasikan oleh assembly, jika tertarik dapat melihat detailnya di runtime/memclr_amd64.s.

Yang perlu disebutkan adalah, jika kode sumber mencoba menggunakan traversing untuk mengosongkan array, misalnya seperti ini

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

Sebelum tidak ada fungsi clear, biasanya seperti ini untuk mengosongkan slice. Saat kompilasi, sekarang kode ini akan dioptimalkan oleh fungsi cmd/compile/internal/walk.arrayRangeClear menjadi bentuk ini

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

Logika masih sama persis dengan di atas, di antaranya ada satu baris i = len(s)-1, fungsinya adalah untuk menghentikan loop setelah memori dibersihkan.

Traversing

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

Saat menggunakan for range untuk traversing slice, akan展开 oleh fungsi walkRange di cmd/compile/internal/walk/range.go menjadi bentuk berikut

go
// Salin struktur
hs := s
// Dapatkan pointer array tingkat rendah
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
}

Dapat dilihat bahwa implementasi for range masih melalui memindahkan pointer untuk traversing elemen. Untuk menghindari slice diperbarui saat traversing, sebelumnya menyalin struktur hs, untuk menghindari pointer menunjuk ke memori yang melampaui batas setelah traversing berakhir, hu menggunakan tipe uintptr untuk menyimpan alamat, saat perlu mengakses elemen baru dikonversi menjadi unsafe.Pointer.

Variabel v2 adalah e di for range, selama seluruh proses traversing dari awal hingga akhir adalah satu variabel, ia hanya akan ditimpa, tidak akan dibuat ulang. Poin ini memicu masalah variabel loop yang membingungkan pengembang Go selama sepuluh tahun, hingga versi go1.21官方 baru akhirnya memutuskan akan menyelesaikannya, diperkirakan dalam pembaruan versi berikutnya, cara pembuatan v2 mungkin akan menjadi seperti berikut.

go
v2 := *hp

Proses pembuatan kode perantara di sini dihilangkan, ini bukan termasuk pengetahuan tentang slice, jika tertarik dapat memahaminya sendiri.

Golang by www.golangdev.cn edit