Bagian dalam Slice

Struct header slice, array penyangga, dan ekspresi full slice.

Daftar isi

Di balik sintaksnya yang ringkas, setiap nilai slice pada dasarnya adalah struct kecil berisi tiga field, yaitu pointer ke array penyangga, panjang, dan kapasitas. Topik ini membedah struktur tersebut beserta akibatnya pada operasi reslice (pengirisan ulang atas slice yang sudah ada), perilaku slice yang dikirim ke fungsi, dan ekspresi full slice (full slice expression) yang membatasi kapasitas. Setelah membacanya, perilaku slice yang tampak tak terduga dapat dijelaskan lewat aritmetika sederhana pada array penyangganya.

Header slice berisi pointer, len, dan cap#

Header slice dapat dibayangkan sebagai struct dengan tiga field seperti berikut. Bentuk ini hanyalah ilustrasi konsep, karena representasi internal yang sesungguhnya tidak ditetapkan spesifikasi:

go Ilustrasi header slice
// tidak-dijalankan
type header struct {
	pointer uintptr // alamat elemen pertama pada array penyangga
	len     int     // banyak elemen yang terlihat
	cap     int     // banyak slot hingga ujung array penyangga
}

Sebuah variabel bertipe slice hanya menyimpan header itu, bukan datanya. Penugasan, pengiriman ke fungsi, dan pengirisan bekerja pada header, sedangkan elemen tetap diam di tempatnya. Diagram berikut memperlihatkan hubungan header bagian, hasil dari nilai[1:3], dengan array penyangganya:

header slicepointerlen 2cap 3array penyangga807590650123cakupan lensisa kapasitas (cap)
Anatomi slice untuk hasil nilai[1:3]: header berisi pointer, len, dan cap

Pointer menunjuk elemen pertama milik slice, yang pada kasus ini bukan elemen pertama array. Field len menghitung dua elemen yang terlihat, sedangkan cap menghitung tiga slot, yaitu dari posisi pointer hingga ujung array. Program berikut menghasilkan keadaan yang sama pada diagram dan membuktikan bahwa kedua slice berbagi elemen fisik:

go reslice.go
package main

import "fmt"

func main() {
	nilai := []int{80, 75, 90, 65}
	bagian := nilai[1:3]

	fmt.Println("bagian :", bagian)
	fmt.Println("len    :", len(bagian))
	fmt.Println("cap    :", cap(bagian))

	bagian[0] = 70
	fmt.Println("nilai  :", nilai)
}
text Keluaran
bagian : [75 90]
len    : 2
cap    : 3
nilai  : [80 70 90 65]

Kapasitas pada irisan sederhana mengikuti rumus cap(asal) - low, dengan low sebagai indeks awal irisan, sehingga nilai[1:3] menghasilkan cap 4 - 1 = 3. Rumus inilah yang menjelaskan mengapa irisan yang menunjuk ke arah ujung array memiliki kapasitas yang menyusut, dan mengapa append pada irisan semacam itu lebih cepat pindah ke array baru. Nilai 70 yang ditulis lewat bagian terbaca penuh oleh nilai karena keduanya menunjuk slot fisik yang sama.

Slice dikirim ke fungsi tetap berbagi data#

Header slice yang disalin saat dikirim ke fungsi tetap menunjuk array penyangga yang sama, sehingga fungsi dapat mengubah elemen pemanggilnya tanpa pointer eksplisit. Kontras ini jelas apabila dibandingkan dengan array, yang disalin utuh ketika dikirim. Program berikut menaikkan seluruh nilai ujian lima poin dari dalam fungsi:

go lewat-fungsi.go
package main

import "fmt"

// naikkan menambah lima pada setiap elemen slice yang diterimanya.
func naikkan(data []int) {
	for i := range data {
		data[i] += 5
	}
}

func main() {
	nilai := []int{80, 75, 90}
	naikkan(nilai)
	fmt.Println("nilai:", nilai)
}
text Keluaran
nilai: [85 80 95]

Fungsi naikkan hanya menerima salinan header berukuran tiga kata memori, tetapi perubahannya terlihat penuh oleh pemanggil karena elemen fisiknya sama. Perilaku inilah alasan sebagian besar fungsi pustaka standar menerima []T dan bukan array, karena biaya pengirimannya konstan sekecil apa pun panjang datanya, sementara perubahan tetap sampai ke pemanggil bila memang diinginkan.

Ekspresi full slice membatasi kapasitas#

Ekspresi full slice menambahkan indeks ketiga berupa max, ditulis s[low:high:max], yang mengikat kapasitas irisan pada batas itu. Kapasitas hasilnya menjadi max - low, alih-alih cap(asal) - low. Dengan mengikat kapasitas tepat pada panjangnya, append kehilangan slot kosong untuk ditulis dan dipaksa pindah ke array penyangga baru sejak elemen pertama yang ditambahkan:

go full-slice.go
package main

import "fmt"

func main() {
	nilai := []int{80, 75, 90, 65}
	aman := nilai[0:2:2]

	fmt.Println("aman:", aman, "len:", len(aman), "cap:", cap(aman))

	aman = append(aman, 60)
	fmt.Println("aman :", aman)
	fmt.Println("nilai:", nilai)
}
text Keluaran
aman: [80 75] len: 2 cap: 2
aman : [80 75 60]
nilai: [80 75 90 65]

Nilai aman menunjuk data yang sama seperti nilai[0:2] biasa, tetapi kapasitasnya terkunci pada angka dua. Indeks ketiga lazim ditulis sama dengan high untuk mengunci kapasitas, atau sama dengan ujung array ketika seluruh sisa slot masih boleh dipakai. append tidak menemukan slot kosong, mengalokasikan array baru, dan nilai tetap tak tersentuh. Bentuk ini menjadi perlindungan yang tepat ketika sebuah irisan diserahkan ke fungsi atau paket lain yang berpotensi melakukan append. Apabila salinan yang lepas sepenuhnya lebih diinginkan, copy ke make yang baru menjadi pilihan.

Ukuran array penyangga juga menyangkut masa hidup memori. Irisan kecil yang menunjuk array besar membuat seluruh array besar tetap tersimpan selama irisannya dipakai, sekalipun hanya sepuluh elemen yang terlihat. Apabila array asalnya jauh lebih besar dari yang dibutuhkan, copy ke slice make yang baru melepas sisa array itu dari memori.

Kesalahan umum#

  1. Kapasitas hasil irisan disalahpahami. Banyak yang menyangka cap irisan selalu sama dengan len-nya. Rumusnya tetap cap(asal) - low, sehingga irisan di tengah array membawa kapasitas besar yang diam-diam mengizinkan append menulis ke slot asal. Perbaikannya, hitung kapasitas dengan rumus itu atau kunci dengan ekspresi full slice.
  2. Indeks max yang melebihi kapasitas asal. Ekspresi s[0:2:9] pada slice berkapasitas empat memicu panic slice bounds out of range saat program berjalan. Perbaikannya, pastikan low <= high <= max <= cap(s).
  3. Menyerahkan irisan tanpa melindungi kapasitasnya. Irisan yang diberikan ke paket lain tetap membuka array penyangga pemanggil, sehingga append di sana dapat menimpa data asal. Perbaikannya, kirim irisan dengan ekspresi full slice atau salin dulu ke array penyangga baru.

Ringkasan#

  • Slice menyimpan header tiga field, yaitu pointer ke array penyangga, len, dan cap, sehingga nilainya ringkas dan cepat ditugaskan.
  • Reslice hanya membuat header baru dengan len dan cap yang dihitung ulang, tanpa menyalin elemen, dengan kapasitas mengikuti rumus cap(asal) - low.
  • Slice dikirim ke fungsi lewat header yang menunjuk data asal, sehingga fungsi dapat mengubah elemen pemanggilnya.
  • Ekspresi full slice s[low:high:max] mengikat cap menjadi max - low dan memaksa append pindah array sejak elemen tambahan pertama.

Referensi#