Slice pada Go: penggunaan dan internal

Andrew Gerrand
5 Januari 2011

Pendahuluan

Tipe slice pada Go menyediakan cara yang mudah dan efisien untuk bekerja dengan seurutan data bertipe. Slice sama dengan array pada bahasa pemrograman lainnya, namun memiliki beberapa properti yang tidak biasa. Artikel ini akan menelaah apa itu slice dan bagaimana cara menggunakannya.

Array

Tipe slice adalah sebuah abstraksi yang dibangun di atas tipe array, jadi untuk memahami slice kita harus memahami array terlebih dahulu.

Definisi tipe dari sebuah array menspesifikasikan panjang dan tipe dari elemen. Contohnya, tipe [4]int merepresentasikan sebuah array dari empat integer. Ukuran dari array tetap; panjangnya adalah bagian dari tipenya ([4]int dan [5]int adalah tipe yang berbeda dan tidak kompatibel). Array bisa diakses dengan metode indeks pada biasanya, sehingga ekspresi s[n] berarti mengakses elemen ke-n, yang mana n dimulai dari nol.

var a [4]int
a[0] = 1
i := a[0]
// i == 1

Array tidak perlu diinisiasi secara eksplisit; nilai kosong dari sebuah array siap digunakan yang setiap elemennya yaitu nilai kosong dari tipe array tersebut:

// a[2] == 0, nilai kosong dari tipe int

Representasi [4]int dalam memori yaitu empat integer yang berurutan:

go-slices-usage-and-internals_slice-array

Array pada Go adalah nilai. Sebuah variabel array menyatakan keseluruhan array: ia bukan pointer ke elemen pertama (seperti halnya pada C). Hal ini berarti bahwa saat kita mengisi atau mengirim nilai array, kita akan membuat salinan dari isinya. (Untuk mengindahkan penyalinan kita bisa mengirim sebuah pointer ke array, namun hal ini berarti sebuah pointer ke sebuah array, bukan sebuah array.) Bayangkan array adalah sebuah bentuk struct dengan indeks bukan dengan field-field yang memiliki nama: sebuah nilai komposit yang berukuran tetap.

b := [2]string{"Penn", "Teller"}

Atau, kita bisa membuat compiler menghitung elemen array secara otomatis:

b := [...]string{"Penn", "Teller"}

Dalam kedua kasus di atas, tipe dari b yaitu [2]string.

Slice

Array ada gunanya, namun tidak fleksibel, sehingga kita jarang melihatnya dalam kode Go. Slice, ada di mana saja. Slice dibentuk dari array untuk menyediakan kemudahan dan kekuatan yang lebih.

Spesifikasi tipe untuk sebuah slice yaitu []T, yang mana T adalah tipe dari elemen slice. Tidak seperti tipe array, tipe slice tidak memiliki panjang.

Sintaksis dari slice dideklarasikan seperti sintaksis array, namun tanpa jumlah elemen:

letters := []string{"a", "b", "c", "d"}

Sebuah slice bisa dibuat dengan fungsi bawaan make, yang memiliki penanda,

func make([]T, len, cap) []T

yang mana T yaitu tipe elemen dari slice yang akan dibuat. Fungsi make menerima sebuah tipe, panjang, dan kapasitas yang opsional. Saat dipanggil, make mengalokasikan sebuah array dan mengembalikan sebuah slice yang mengacu pada array tersebut.

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

Bila argumen kapasitas diindahkan, ia akan sama nilainya dengan panjang yang dispesifikasikan. Berikut versi singkat dari kode yang sama:

s := make([]byte, 5)

Panjang dan kapasitas dari sebuah slice dapat diketahui menggunakan fungsi bawaan len dan cap.

len(s) == 5
cap(s) == 5

Dua bagian berikut akan mendiskusikan hubungan antara panjang dan kapasitas.

Nilai kosong dari sebuah slice adalah nil. Fungsi len dan cap akan mengembalikan nilai 0 untuk slice yang nil.

Sebuah slice juga dapat dibentuk dengan "memotong" slice atau array. Pemotongan dilakukan dengan menspesifikasikan rentang setengah-terbuka dengan dua indeks yang dipisahkan oleh tanda titik-dua. Contohnya, ekspresi b[1:4] membuat sebuah slice yang mengikutkan elemen 1 sampai 3 dari b (indeks dari pemotongan slice yaitu tetap dari 0 sampai 2).

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, berbagi penyimpanan yang sama dengan b.

Indeks awal dan akhir dari ekspresi pemotongan slice tidak harus diisi; nilai bakunya yaitu nol dan panjang dari slice itu sendiri:

// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b

Berikut sintaksis untuk membuat sebuah slice dari sebuah array:

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // sebuah slice yang mengacu penyimpanan dari x.

Internal dari slice

Sebuah slice yaitu descriptor (yang mendeskripsikan) segmen dari array. Ia terdiri dari pointer ke array, panjang dari segmen, dan kapasitasnya (panjang maksimum dari segmen).

go-slices-usage-and-internals_slice-struct

Variabel s yang kita buat sebelumnya dengan make([]byte, 5), memiliki struktur seperti berikut:

go-slices-usage-and-internals_slice-1

Panjangnya yaitu jumlah elemen yang diacu oleh slice. Kapasitasnya yaitu jumlah elemen pada array di belakangnya (dimulai dari elemen pertama yang diacu oleh pointer pada slice). Perbedaan antara panjang dan kapasitas akan terlihat jelas saat kita melihat contoh-contoh selanjutnya.

Saat kita memotong slice s, perhatikan perubahan pada struktur data slice dan hubungannya dengan array di baliknya:

s = s[2:4]

go-slices-usage-and-internals_slice-2

Memotong slice tidak menyalin data dari slice. Ia membuat sebuah nilai slice yang baru yang menunjuk ke array aslinya. Hal ini membuat operasi slice efisien seperti memanipulasi indeks dari array. Oleh karena itu, mengubah elemen (bukan slice itu sendiri) dari hasil pemotongan slice akan mengubah elemen di slice aslinya:

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

Sebelumnya kita memotong s dengan panjang yang lebih kecil dari kapasitasnya. Kita dapat mengembangkan s sampai ke kapasitasnya dengan memotongnya kembali:

s = s[:cap(s)]

go-slices-usage-and-internals_slice-3

Sebuah slice tidak dapat mengembang lebih dari kapasitasnya. Mencoba melakukan hal tersebut akan menyebabkan panik runtime, seperti saat melakukan pengindeksan di luar batas dari slice atau array. Hal yang serupa, slice tidak bisa dipotong kecil dari nol untuk mengakses elemen sebelumnya dalam array.

Mengembangkan slice (fungsi copy dan append)

Untuk meningkatkan kapasitas dari sebuah slice kita harus membuat slice yang baru dan lebih besar dan menyalin isi dari slice asli ke dalamnya. Teknik ini adalah cara implementasi array secara dinamis pada bahasa pemrograman lain. Contoh selanjutnya melipatgandakan kapasitas dari s dengan membuat slice baru t, menyalin isi dari s ke t, dan kemudian menempatkan nilai slice t ke s:

t := make([]byte, len(s), (cap(s)+1)*2) // +1 seandainya cap(s) == 0
for i := range s {
	t[i] = s[i]
}
s = t

Bagian pengulangan pada operasi di atas dapat dipermudah dengan fungsi bawaan copy. Seperti namanya, copy menyalin data dari slice sumber ke slice tujuan. Ia mengembalikan jumlah elemen yang disalin.

func copy(dst, src []T) int

Fungsi copy mendukung penyalinan antara slice yang berbeda panjangnya (ia hanya akan menyalin sampai jumlah elemen paling kecil). Sebagai tambahan, copy dapat menangani slice sumber dan tujuan yang berbagi array yang sama, menangani slice yang saling timpa dengan benar.

Dengan menggunakan copy, kita dapat menyederhanakan potongan kode di atas:

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

Operasi umum lainnya dari slice yaitu menambahkan data ke akhir slice. Fungsi berikut menambahkan elemen byte ke sebuah slice dari byte, mengembangkan slice jika perlu, dan mengembalikan nilai slice yang diperbarui:

func AppendByte(slice []byte, data ...byte) []byte {
	m := len(slice)
	n := m + len(data)
	if n > cap(slice) { // jika perlu, alokasi ulang.
		// Buat alokasi dua kali lebih besar dari yang dibutuhkan,
		// untuk penambahan nantinya.
		newSlice := make([]byte, (n+1)*2)
		copy(newSlice, slice)
		slice = newSlice
	}
	slice = slice[0:n]
	copy(slice[m:n], data)
	return slice
}

Kita gunakan fungsi AppendByte seperti berikut:

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

Fungsi seperti AppendByte berguna karena memberikan kontrol sepenuhnya dalam mengembangkan isi dari slice. Bergantung pada karakteristik program, fungsi tersebut bisa saja mengalokasikan potongan yang lebih kecil atau besar, atau mengatur batas atas dari ukuran realokasi.

Namun kebanyakan program tidak perlu kontrol sepenuhnya, sehingga Go menyediakan fungsi bawaan append yang berguna untuk tujuan umum; fungsi append memiliki penanda

func append(s []T, x ...T) []T

Fungsi append menambahkan elemen-elemen x ke akhir dari slice s, dan mengembangkan ukuran slice jika kapasitas lebih besar dibutuhkan.

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

Untuk menambahkan slice ke slice lainnya, gunakan …​ untuk memperluas argumen kedua menjadi sebuah daftar argumen.

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // sama dengan "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

Secara nilai kosong dari slice (nil) sifatnya seperti slice dengan panjang nol, kita dapat mendeklarasikan sebuah variabel slice dan kemudian menambahkan elemen-elemen dalam sebuah pengulangan:

// Filter mengembalikan sebuah slice baru yang menyimpan hanya elemen-elemen
// dari s yang memenuhi fungsi fn().
func Filter(s []int, fn func(int) bool) []int {
	var p []int // == nil
	for _, v := range s {
		if fn(v) {
			p = append(p, v)
		}
	}
	return p
}

Kesalahan yang umum

Seperti yang disebutkan sebelumnya, memotong sebuah slice tidak menyalin array di belakangnya. Array yang utuh tetap tersimpan dalam memori sampai tidak ada lagi yang memakainya. Terkadang hal ini bisa membuat program menyimpan semua data di dalam memori saat hanya sebagian kecil dari slice yang dibutuhkan.

Sebagai contohnya, fungsi FindDigits berikut memuat sebuah berkas ke dalam memori dan mencari seurutan digit numerik yang pertama, dan mengembalikan urutan tersebut sebagai sebuah slice yang baru.

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
	b, _ := ioutil.ReadFile(filename)
	return digitRegexp.Find(b)
}

Kode di atas berjalan seperti yang tertulis, namun []byte yang dikembalikan menunjuk ke array yang berisi seluruh berkas. Karena slice mengacu ke array aslinya, selama slice tersebut masih digunakan maka garbage collector tidak dapat menghapus array; beberapa byte yang terpakai dari berkas menahan seluruh isi berkas di dalam memori.

Untuk memperbaiki permasalahan ini kita dapat menyalin data yang perlu saja ke slice yang baru sebelum dikembalikan:

func CopyDigits(filename string) []byte {
	b, _ := ioutil.ReadFile(filename)
	b = digitRegexp.Find(b)
	c := make([]byte, len(b))
	copy(c, b)
	return c
}

Versi lebih ringkas dari fungsi di atas dapat dibangun menggunakan append. Cara ini adalah latihan bagi pembaca.

Bacaan Lebih Lanjut

Efektif Go berisi perlakuan lebih dalam dari slice dan array, dan spesifikasi bahasa Go mendefinisikan slice dan fungsi-fungsi pembantu yang berhubungan dengan slice.