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:
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).
Variabel s
yang kita buat sebelumnya dengan make([]byte, 5)
, memiliki
struktur seperti berikut:
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]
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)]
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.