Kita sudah melihat gejala aneh di chapter sebelumnya: dua slice yang berasal dari pemotongan yang sama bisa saling memengaruhi. Gejala tersebut bukan kebetulan, melainkan konsekuensi desain slice di Go. Setiap slice hanyalah penunjuk ke sebuah array penyimpanan di belakangnya — disebut backing array — beserta dua angka len dan cap. Memahami hubungan ini adalah syarat membaca kode slice tingkat lanjut dan menghindari bug aliasing yang sulit dilacak.
Slice dan Array di Baliknya #
Sebuah variabel slice menyimpan tiga hal: alamat awal area data di dalam backing array, panjang len, dan kapasitas cap. Variabel slice itu sendiri tidak menyimpan elemen; elemen tinggal di backing array yang bisa dipakai bersama oleh banyak slice. Pemotongan a[1:4] dan a[2:5] sama-sama membuat slice baru yang menunjuk ke area di dalam backing array a, seperti ilustrasi berikut:
indeks : 0 1 2 3 4
a : [1] [2] [3] [4] [5]
s1 : [2 3 4] a[1:4] len 3 cap 4
s2 : [3 4 5] a[2:5] len 3 cap 3Karena s1 dan s2 menampilkan elemen pada indeks 2 dan 3 dari array yang sama, penulisan lewat salah satunya langsung terlihat oleh yang lain. Perilaku ini disebut aliasing: dua nama mengacu pada memori yang sama.
Menulis Lewat Satu Slice Terlihat dari yang Lain #
Pada program utuh di bawah, s2[0] = 99 menimpa slot indeks 2 dari backing array a. Slot tersebut adalah elemen ketiga a, elemen kedua s1, sekaligus elemen pertama s2 — ketiganya menampilkan angka 99 setelah penulisan itu. Ini bukan bug melainkan konsekuensi langsung dari berbagi penyimpanan.
append di Dalam Kapasitas Menimpa Slot Milik Slice Lain #
Jebakan yang lebih halus muncul saat append masih berada di dalam kapasitas: elemen baru ditulis ke slot sisa di backing array, padahal slot itu bisa menjadi milik jendela slice lain. Program di bawah membuat b dengan len 3 cap 6, lalu sisiLain := b[:3] menampilkan tiga elemen yang sama. Ketika sisiLain menambah elemen 80, angka itu menempati slot keempat backing array. Ketika b kemudian menambah elemen 70, b juga menulis pada slot keempat yang sama dan menimpa 80 — nilai 80 menghilang tanpa pesan galat, dan sisiLain ikut berubah menjadi [1 2 3 70].
Solusi: Salin dan Batasi #
Dua alat menetralkan risiko aliasing. Pertama, copy ke slice baru yang dialokasikan make, sehingga data berdiri di penyimpanan terpisah; modifikasi salinan tidak lagi menyentuh slice asal. Kedua, full slice expression a[low:high:max] membatasi kapasitas hasil pemotongan menjadi max - low — potongan a[1:4:4] memiliki len 3 cap 3 sehingga append berikutnya dipaksa melewati batas dan otomatis pindah ke penyimpanan baru, tidak lagi menimpa backing array a.
Program Utuh #
Berkas backing-array-demo.go mendemokan keempat skenario tersebut secara berurutan. Jalankan dengan go run backing-array-demo.go dari direktori chapter:
package main
import "fmt"
func main() {
// a menjadi backing array yang dibagi oleh s1 dan s2.
a := []int{1, 2, 3, 4, 5}
s1 := a[1:4] // [2 3 4], berbagi backing array dengan a
s2 := a[2:5] // [3 4 5], berbagi backing array dengan a
fmt.Println("awal s1:", s1, "s2:", s2)
// Tulis lewat s2, perubahan terlihat dari s1 dan a.
s2[0] = 99
fmt.Println("s2[0] = 99")
fmt.Println("s1 :", s1)
fmt.Println("s2 :", s2)
fmt.Println("a :", a)
// append di dalam kapasitas menimpa slot backing array milik slice lain.
b := make([]int, 3, 6)
b[0], b[1], b[2] = 1, 2, 3
sisiLain := b[:3]
sisiLain = append(sisiLain, 80)
fmt.Println("setelah sisiLain append 80:", sisiLain)
b = append(b, 70)
fmt.Println("setelah b append 70 :", b)
fmt.Println("sisiLain :", sisiLain)
// Solusi: salin agar tidak berbagi backing array.
salinan := make([]int, len(s1))
copy(salinan, s1)
salinan[0] = 7
fmt.Println("salinan[0] = 7")
fmt.Println("s1 :", s1)
fmt.Println("salinan:", salinan)
// Full slice expression a[low:high:max] membatasi cap.
terbatas := a[1:4:4] // len 3, cap 3, tidak bisa tumbuh
terbatas = append(terbatas, 500)
fmt.Println("terbatas:", terbatas)
fmt.Println("a :", a)
}Output yang Dihasilkan:
awal s1: [2 3 4] s2: [3 4 5]
s2[0] = 99
s1 : [2 99 4]
s2 : [99 4 5]
a : [1 2 99 4 5]
setelah sisiLain append 80: [1 2 3 80]
setelah b append 70 : [1 2 3 70]
sisiLain : [1 2 3 70]
salinan[0] = 7
s1 : [2 99 4]
salinan: [7 99 4]
terbatas: [2 99 4 500]
a : [1 2 99 4 5]Mari kita telaah baris demi baris program di atas:
- Baris 7–9 (
s1 := a[1:4],s2 := a[2:5]): kedua potongan dibuat dari backing arrayayang sama; area keduanya bertumpuk pada indeks 2 dan 3. - Baris 12 (
s2[0] = 99): menulis slot indeks 2 daria; output membuktikans1,s2, danasama-sama melihat99karena ketiganya menampilkan slot yang sama. - Baris 19–21 (
sisiLain = append(sisiLain, 80)):sisiLainmenulis80ke slot keempat backing arraybkarena kapasitas masih tersisa. - Baris 22–24 (
b = append(b, 70)):bmenulis70pada slot keempat yang sama, menimpa80; baris berikutnya membuktikansisiLainikut berubah menjadi[1 2 3 70]. - Baris 27–31 (
copy(salinan, s1)): salinan berdiri di penyimpanan baru sehinggasalinan[0] = 7tidak mengubahs1yang tetap[2 99 4]. - Baris 34–36 (
a[1:4:4]): batasmaxmembuat kapasitas potongan habis tepat di indeks 4, sehinggaappend(terbatas, 500)memicu realokasi danatidak tersentuh — output terakhir menunjukkanatetap[1 2 99 4 5].
Dengan konsep backing array, perilaku slice di seluruh chapter sebelumnya akhirnya masuk akal: len mengukur jendela, cap mengukur sisa ruang di penyimpanan, dan append menulis ke slot nyata yang bisa dipakai bersama. Berikutnya kita menyusun data berbentuk tabel dan matriks dengan slice bersarang.
Referensi #
- Go Blog: Arrays, slices (and strings): The mechanics of append — sumber utama konsep backing array dan full slice expression.
- Spesifikasi Go: Slice types — definisi resmi tipe slice.
- Go Blog: Slices: usage and internals (slices) — struktur internal slice dan growth.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
backing-array-demo.go
Unduh berkaspackage main
import "fmt"
func main() {
// a menjadi backing array yang dibagi oleh s1 dan s2.
a := []int{1, 2, 3, 4, 5}
s1 := a[1:4] // [2 3 4], berbagi backing array dengan a
s2 := a[2:5] // [3 4 5], berbagi backing array dengan a
fmt.Println("awal s1:", s1, "s2:", s2)
// Tulis lewat s2, perubahan terlihat dari s1 dan a.
s2[0] = 99
fmt.Println("s2[0] = 99")
fmt.Println("s1 :", s1)
fmt.Println("s2 :", s2)
fmt.Println("a :", a)
// append di dalam kapasitas menimpa slot backing array milik slice lain.
b := make([]int, 3, 6)
b[0], b[1], b[2] = 1, 2, 3
sisiLain := b[:3]
sisiLain = append(sisiLain, 80)
fmt.Println("setelah sisiLain append 80:", sisiLain)
b = append(b, 70)
fmt.Println("setelah b append 70 :", b)
fmt.Println("sisiLain :", sisiLain)
// Solusi: salin agar tidak berbagi backing array.
salinan := make([]int, len(s1))
copy(salinan, s1)
salinan[0] = 7
fmt.Println("salinan[0] = 7")
fmt.Println("s1 :", s1)
fmt.Println("salinan:", salinan)
// Full slice expression a[low:high:max] membatasi cap.
terbatas := a[1:4:4] // len 3, cap 3, tidak bisa tumbuh
terbatas = append(terbatas, 500)
fmt.Println("terbatas:", terbatas)
fmt.Println("a :", a)
}