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:
// 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:
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:
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)
}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:
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)
}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:
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)
}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#
- Kapasitas hasil irisan disalahpahami. Banyak yang menyangka
capirisan selalu sama denganlen-nya. Rumusnya tetapcap(asal) - low, sehingga irisan di tengah array membawa kapasitas besar yang diam-diam mengizinkanappendmenulis ke slot asal. Perbaikannya, hitung kapasitas dengan rumus itu atau kunci dengan ekspresi full slice. - Indeks
maxyang melebihi kapasitas asal. Ekspresis[0:2:9]pada slice berkapasitas empat memicu panicslice bounds out of rangesaat program berjalan. Perbaikannya, pastikanlow <= high <= max <= cap(s). - Menyerahkan irisan tanpa melindungi kapasitasnya. Irisan yang diberikan ke paket lain tetap membuka array penyangga pemanggil, sehingga
appenddi 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, dancap, sehingga nilainya ringkas dan cepat ditugaskan. - Reslice hanya membuat header baru dengan
lendancapyang dihitung ulang, tanpa menyalin elemen, dengan kapasitas mengikuti rumuscap(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]mengikatcapmenjadimax - lowdan memaksaappendpindah array sejak elemen tambahan pertama.
Referensi#
- Arrays, slices (and strings): The mechanics of ‘append’ — The Go Blog — Menguraikan relasi slice dengan array penyangga beserta aritmetika append.
- The Go Programming Language Specification — The Go Authors — Menetapkan irisan sederhana dan full slice expression dengan tiga indeks.
- Go Slices: usage and internals — The Go Blog — Menggambarkan struct header slice dan perannya pada operasi umum.