Slice menjadi kumpulan data utama di Go karena ukurannya dapat tumbuh, tetapi fleksibilitas itu dikelola melalui dua angka yang melekat pada setiap slice, yaitu panjang (len) dan kapasitas (cap). Topik ini membahas makna keduanya, cara make menyiapkan kapasitas, pola pertumbuhan yang dilakukan append ketika kapasitas habis, serta perilaku aliasing ketika dua slice berbagi satu array penyangga. Perilaku terakhir adalah sumber bug yang paling sering dialami pemula, sehingga penyebab dan perbaikannya diperlihatkan dengan program yang lengkap.
len dan cap#
Array penyangga (backing array) adalah array tempat elemen slice benar-benar disimpan. Fungsi len menghitung banyak elemen yang sedang terlihat pada slice, sedangkan cap menghitung banyak slot yang tersedia pada array penyangganya sebelum slice harus pindah ke array baru. Keduanya dapat diatur saat pembuatan dengan make: bentuk make([]int, 3, 5) membuat slice dengan tiga elemen siap pakai dan lima slot total, sedangkan make([]int, 3) menyamakan keduanya pada angka tiga.
package main
import "fmt"
func main() {
s := make([]int, 3, 5)
fmt.Println("slice :", s)
fmt.Println("len :", len(s))
fmt.Println("cap :", cap(s))
}slice : [0 0 0]
len : 3
cap : 5Tiga elemen yang dibuat make langsung berisi 0 karena zero value int, sehingga slice tersebut dapat diindeks segera tanpa append. Dua slot sisanya tetap tersedia meskipun tidak terlihat, dan itulah wilayah yang dihitung oleh cap tetapi tidak dihitung oleh len. Selisih cap dikurangi len menyatakan banyak slot kosong yang dapat diisi append tanpa memindahkan data.
Pertumbuhan append#
Fungsi append menambah elemen ke ujung slice dan mengembalikan slice hasilnya, yang bisa jadi menunjuk array penyangga yang sama maupun array baru. Apabila cap masih longgar, elemen baru ditulis ke slot kosong yang sudah ada. Apabila cap habis, append mengalokasikan array penyangga yang lebih besar, menyalin seluruh elemen lama, lalu mengembalikan slice yang menunjuk array baru itu:
package main
import "fmt"
func main() {
s := []int{}
for i := 1; i <= 8; i++ {
s = append(s, i*i)
fmt.Printf("len %2d cap %2d\n", len(s), cap(s))
}
fmt.Println("isi:", s)
}len 1 cap 1
len 2 cap 2
len 3 cap 4
len 4 cap 4
len 5 cap 8
len 6 cap 8
len 7 cap 8
len 8 cap 8
isi: [1 4 9 16 25 36 49 64]Pada Go 1.27 di lingkungan linux/amd64, kapasitas tumbuh mendekati penggandaan pada slice kecil, yaitu 1, 2, 4, lalu 8. Pola penggandaan ini membuat penambahan satu per satu tetap efisien secara rata-rata, karena perpindahan data yang mahal terjadi semakin jarang seiring slice bertambah besar. Angka pastinya bukan jaminan spesifikasi bahasa, dan versi Go yang berbeda dapat memilih angka pertumbuhan yang berbeda pula, sehingga logika program tidak boleh bergantung pada nilai cap yang spesifik.
Program dijalankan dari folder kerja tetap:
# siapkan folder kerja untuk topik ini
mkdir -p /tmp/gotipe/slice-mendalam
cp len-cap.go pertumbuhan.go aliasing.go salin-aman.go /tmp/gotipe/slice-mendalam/
cd /tmp/gotipe/slice-mendalam
# periksa format lalu jalankan program pertumbuhannya
gofmt -l .
go run pertumbuhan.golen 1 cap 1
len 2 cap 2
len 3 cap 4
len 4 cap 4
len 5 cap 8
len 6 cap 8
len 7 cap 8
len 8 cap 8
isi: [1 4 9 16 25 36 49 64]Dua slice berbagi satu array penyangga#
Mengiris slice tidak menyalin datanya. Ekspresi nilai[0:2] hanya membuat header baru, yaitu penanda kecil berisi lokasi data, yang menunjuk ke array penyangga yang sama dengan len dan cap yang dihitung ulang. Akibatnya, dua slice dapat menulis ke elemen fisik yang sama, dan perubahan lewat salah satunya terlihat pada yang lain. Program berikut memperlihatkan urutan kejadian dua tahap:
package main
import "fmt"
func main() {
nilai := []int{80, 75, 90, 65}
bagian := nilai[0:2]
bagian[0] = 85
fmt.Println("setelah mengubah bagian[0]")
fmt.Println("nilai :", nilai)
fmt.Println("bagian:", bagian)
bagian = append(bagian, 60)
fmt.Println("setelah append")
fmt.Println("nilai :", nilai)
fmt.Println("bagian:", bagian)
fmt.Println("cap :", cap(bagian))
}setelah mengubah bagian[0]
nilai : [85 75 90 65]
bagian: [85 75]
setelah append
nilai : [85 75 60 65]
bagian: [85 75 60]
cap : 4Tahap pertama masih mudah dijelaskan, yaitu bagian[0] = 85 mengubah elemen yang memang berbagi lokasi fisik, sehingga nilai ikut berubah. Tahap kedua lebih mengejutkan. Saat append menambahkan 60, kapasitas bagian masih empat dengan len dua, sehingga append menulis ke slot kosong array penyangga yang sama, tepat pada indeks 2. Elemen 90 milik nilai tertimpa menjadi 60 tanpa pesan error apa pun.
Perbaikan yang paling langsung adalah menyalin elemen ke array penyangga baru dengan fungsi copy. Bentuk copy(tujuan, sumber) menyalin sebanyak elemen terkecil dari kedua sisinya, sehingga tujuan sebaiknya disiapkan dengan make berukuran tepat:
package main
import "fmt"
func main() {
nilai := []int{80, 75, 90, 65}
bagian := make([]int, 2)
copy(bagian, nilai[0:2])
bagian[0] = 85
fmt.Println("nilai :", nilai)
fmt.Println("bagian:", bagian)
}nilai : [80 75 90 65]
bagian: [85 75]Setelah disalin, bagian memiliki array penyangga sendiri, sehingga perubahan dan penambahan tidak menyentuh nilai lagi. Fungsi copy mengembalikan banyak elemen yang benar-benar tersalin, yaitu yang terkecil antara kedua sisinya, sehingga hasilnya dapat dipakai untuk memeriksa apakah salinannya terpotong. Alternatif lain adalah ekspresi full slice dengan tiga indeks, yaitu bentuk iris yang mengikat kapasitas sehingga append langsung pindah array, dan diuraikan menyeluruh pada topik Bagian dalam Slice.
Kesalahan umum#
- Hasil
appendtidak ditugaskan. Pernyataanappend(s, 10)yang berdiri sendiri ditolak kompiler dengan pesanevaluated but not used, karena fungsi itu mengembalikan slice yang bisa jadi baru. Perbaikannya, selalu tuliss = append(s, 10)agar hasilnya kembali ke variabel. - Mengira penugasan slice menyalin elemennya. Pernyataan
lain := nilaihanya menyalin header, sehingga keduanya tetap berbagi array penyangga. Perbaikannya, gunakancopyke slicemakeyang baru apabila salinan yang lepas dibutuhkan. - Keliru memilih bentuk
make.make([]int, 3)membuat tiga elemen berisi nol, sedangkanmake([]int, 0, 3)membuat nol elemen dengan kapasitas tiga. Pengisian lewatappendpada bentuk pertama menambah elemen setelah tiga nol tadi. Perbaikannya, pilih bentuk sesuai maksud, yaitu panjang untuk data yang siap diindeks dan kapasitas untuk penampung yang akan diisiappend.
Ringkasan#
lenmenyatakan banyak elemen yang terlihat, sedangkancapmenyatakan banyak slot pada array penyangga hingga ujungnya.appendmenulis ke slot kosong selama masih ada, dan memindahkan data ke array yang lebih besar saat kapasitas habis.- Pola pertumbuhan mendekati penggandaan pada slice kecil, tetapi angkanya bukan jaminan spesifikasi dan dapat berubah antar versi Go.
- Irisan dan asalnya berbagi array penyangga, sehingga perubahan maupun
appendpada irisan dapat menyentuh data asal, dancopymenjadi pelindung bila data asal harus terjaga.
Referensi#
- Go Slices: usage and internals — The Go Blog — Memaparkan len, cap, dan array penyangga beserta mekanisme append.
- Arrays, slices (and strings): The mechanics of ‘append’ — The Go Blog — Menelusuri langkah append ketika kapasitas habis dan data harus dipindahkan.
- The Go Programming Language Specification — The Go Authors — Menetapkan semantik append dan copy beserta nilai kembaliannya.