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:

text
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 3

Karena 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.

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:

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

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

  1. Baris 7–9 (s1 := a[1:4], s2 := a[2:5]): kedua potongan dibuat dari backing array a yang sama; area keduanya bertumpuk pada indeks 2 dan 3.
  2. Baris 12 (s2[0] = 99): menulis slot indeks 2 dari a; output membuktikan s1, s2, dan a sama-sama melihat 99 karena ketiganya menampilkan slot yang sama.
  3. Baris 19–21 (sisiLain = append(sisiLain, 80)): sisiLain menulis 80 ke slot keempat backing array b karena kapasitas masih tersisa.
  4. Baris 22–24 (b = append(b, 70)): b menulis 70 pada slot keempat yang sama, menimpa 80; baris berikutnya membuktikan sisiLain ikut berubah menjadi [1 2 3 70].
  5. Baris 27–31 (copy(salinan, s1)): salinan berdiri di penyimpanan baru sehingga salinan[0] = 7 tidak mengubah s1 yang tetap [2 99 4].
  6. Baris 34–36 (a[1:4:4]): batas max membuat kapasitas potongan habis tepat di indeks 4, sehingga append(terbatas, 500) memicu realokasi dan a tidak tersentuh — output terakhir menunjukkan a tetap [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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

backing-array-demo.go

Unduh berkas
go
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)
}