Pendahuluan
Salah satu fitur paling umum dari bahasa pemrograman prosedural yaitu konsep dari sebuah array (larik). Array tampak seperti hal yang simpel namun ada beberapa pertanyaan yang harus dijawab saat menambahkan array ke dalam sebuah bahasa pemrograman, seperti:
-
apakah ukuran array tetap atau dinamis?
-
apakah ukuran bagian dari tipe?
-
seperti apa bentuk dari array multi dimensi?
-
apakah array kosong ada maknanya?
Jawaban dari pertanyaan tersebut memengaruhi apakah array adalah sebuah fitur atau bagian inti dari rancangan bahasa (pemrograman).
Pada awal pengembangan Go, dibutuhkan sekitar setahun untuk memutuskan jawaban dari pertanyaan tersebut sebelum rancangannya dirasakan tepat. Langkah kuncinya adalah pengenalan dari slice, yang dibangun dari array yang berukuran tetap untuk memberikan struktur data yang fleksibel dan mudah dikembangkan. Sampai sekarang, pemrogram yang baru dengan Go sering kali terbentur dengan cara kerja slice, bisa jadi karena pengalaman dari bahasa pemrograman lain mengaburkan cara berpikir mereka.
Dalam artikel ini, kita akan mencoba menjernihkan kebingungan ini,
dengan cara membangun bagian-bagian tersebut untuk
menjelaskan bagaimana fungsi bawaan append
bekerja, dan kenapa ia bekerja
seperti itu.
Array
Array adalah blok pembangun yang penting dalam Go, namun seperti halnya fondasi dalam sebuah bangunan mereka tersembunyi di bawah komponen-komponen yang lebih terlihat. Kita harus membahas array terlebih dahulu sebelum membahas tentang slice.
Array tidak terlalu sering terlihat dalam program Go karena ukuran dari sebuah array adalah bagian dari tipenya, yang membatasi ekspresi array itu sendiri.
Deklarasi
var buffer [256]byte
mendeklarasikan variabel buffer
yang menyimpan 256 byte.
Tipe dari buffer
mengikutkan ukurannya, [256]byte
.
Sebuah array dengan 512 byte akan memiliki tipe berbeda yaitu [512]byte
.
Data yang berasosiasi dengan sebuah array yaitu: deretan dari elemen.
Secara skematis, buffer
tersebut bentuknya seperti ini dalam memory,
buffer: byte byte byte ... 256 kali ... byte byte byte
Variabel buffer
dapat menampung sebanyak 256 byte data, tidak lebih.
Kita dapat mengakses elemen array dengan sintaksis pengindeksan yang umum,
buffer[0]
, buffer[1]
, dan seterusnya sampai buffer[255]
.
(Rentang indeks 0 sampai 255 melingkupi 256 elemen.)
Mencoba mengakses indeks buffer
dengan nilai di luar rentang tersebut akan
menyebabkan program menjadi crash.
Ada fungsi bawaan yang disebut dengan len
yang mengembalikan jumlah elemen
dari sebuah array atau slice dan beberapa tipe data lainnya.
Untuk array, cukup jelas nilai kembalian dari len
.
Pada contoh di atas, len(buffer)
mengembalikan nilai tetap 256.
Array ada gunanya—misalnya mereka adalah representasi yang bagus untuk sebuah transformasi matriks—namun tujuan umum mereka dalam Go adalah sebagai tempat penyimpanan untuk sebuah slice.
Slice: header dari slice
Untuk menggunakan slice dengan benar kita harus memahami apa itu slice dan apa yang ia lakukan.
Sebuah slice adalah sebuah struktur data yang berisi sebuah array yang tersimpan terpisah dari variabel slice itu sendiri. Sebuah slice bukanlah sebuah array. Slice berisi bagian dari array.
Dari variabel array buffer
sebelumnya, kita dapat mengambil elemen 100
sampai 150 (lebih tepatnya, 100 sampai 149, secara inklusif) dengan memotong
array tersebut:
var slice []byte = buffer[100:150]
Dalam potongan kode tersebut kita menggunakan deklarasi variabel supaya
lebih eksplisit.
Variabel bernama slice
memiliki tipe []byte
, disebut dengan "slice dari
byte", yang diinisiasi dari array bernama buffer
, dengan memotong elemen
100 (inklusif) sampai 150 (eksklusif).
Sintaksis yang lebih idiomatis tanpa menggunakan tipe, ekspresinya adalah:
var slice = buffer[100:150]
Dalam sebuah fungsi kita dapat menggunakan bentuk deklarasi singkat,
slice := buffer[100:150]
Apa sebenarnya variabel slice ini? Penjelaskan kita belum lengkap saat ini, namun untuk sekarang bayangkan sebuah slice sebagai sebuah struktur data kecil dengan dua elemen: sebuah panjang dan sebuah pointer ke elemen dari sebuah array. Anda dapat membayangkan slice di belakangnya berbentuk seperti ini:
type sliceHeader struct { Length int ZerothElement *byte } slice := sliceHeader{ Length: 50, ZerothElement: &buffer[100], }
Tentu saja, bagian kode di atas hanyalah sebuah ilustrasi.
Struct dari sliceHeader
tidak terlihat oleh programmer, dan tipe dari elemen
pointer bergantung pada tipe dari elemen yang ditunjuk, namun hal ini sudah
cukup memberikan gambaran umum dari mekanisme slice.
Sejauh ini kita telah menggunakan operasi slice pada sebuah array, namun kita juga dapat memotong sebuah slice, seperti berikut:
slice2 := slice[5:10]
Operasi ini membuat sebuah slice yang baru, dengan elemen 5 sampai 9
(inklusif) dari slice aslinya, yang artinya elemen 105 sampai 109 dari array
yang aslinya.
Struct sliceHeader
untuk variabel slice2
bentuknya seperti berikut:
slice2 := sliceHeader{ Length: 5, ZerothElement: &buffer[105], }
Perhatikan bahwa header tersebut masih menunjuk ke dasar array yang sama, yang
disimpan dalam variabel buffer
.
Kita juga dapat memotong ulang, bisa dikatakan memotong sebuah slice dan menyimpan hasilnya kembali ke struktur slice aslinya. Setelah
slice = slice[5:10]
struktur dari sliceHeader
untuk variabel slice
akan seperti variabel
slice2
.
Anda akan sering melihat bentuk pemotongan ulang slice ini digunakan, misalnya
untuk menyingkat sebuah slice.
Pernyataan berikut memotong elemen pertama dan terakhir dari slice:
slice = slice[1:len(slice)-1]
(Latihan: tulis lah bentuk struct dari sliceHeader
setelah pernyataan
tersebut.)
Anda akan sering mendengar pemrogram Go yang berpengalaman berbicara tentang "header slice" karena itulah yang disimpan dalam sebuah variabel slice. Contohnya, saat Anda memanggil sebuah fungsi yang menerima sebuah slice sebagai argumen, seperti bytes.IndexRune, header itulah yang dikirim ke fungsi. Dalam pemanggilan berikut,
slashPos := bytes.IndexRune(slice, '/')
argumen slice
yang dikirim ke fungsi IndexRune
adalah sebuah "header
slice".
Ada sebuah data lagi dalam header dari slice, yang akan kita bahas di bawah, namun pertama mari kita lihat arti dari header slice saat membuat program dengan slice.
Mengirim slice ke fungsi
Sangatlah penting untuk memahami bahwa meskipun sebuah slice berisi sebuah pointer, slice itu sendiri adalah sebuah nilai. Di balik nilai tersebut adalah sebuah struct yang menyimpan sebuah pointer dan sebuah panjang (array). Bukan sebuah pointer ke sebuah struct.
Hal ini penting.
Saat kita memanggil IndexRune
pada contoh sebelumnya, slice dikirim sebagai
sebuah salinan dari header slice.
Perilaku ini memiliki pengaruh yang penting.
Pertimbangkan fungsi sederhana berikut:
func AddOneToEachElement(slice []byte) { for i := range slice { slice[i]++ } }
Fungsi tersebut mengiterasi sebuah slice lewat indeks (menggunakan pengulangan
for range
), dan meningkatkan nilai setiap elemennya dengan satu.
Cobalah:
func main() { slice := buffer[10:20] for i := 0; i < len(slice); i++ { slice[i] = byte(i) } fmt.Println("before", slice) AddOneToEachElement(slice) fmt.Println("after", slice) }
(Anda bisa mengubah dan mengeksekusi ulang potongan kode di atas jika Anda ingin eksplorasi lebih lanjut.)
Walaupun header dari slice dikirim secara nilai (pass by value), header tersebut mengandung sebuah pointer ke elemen dari array, sehingga header dari slice yang asli dan header yang dikirim ke fungsi menunjuk ke array yang sama. Oleh karena itu, saat fungsi selesai, elemen yang berubah dapat dilihat lewat variabel slice yang asli.
Argumen pada fungsi adalah sebuah salinan, seperti yang diperlihatkan contoh berikut:
func SubtractOneFromLength(slice []byte) []byte { slice = slice[0 : len(slice)-1] return slice } func main() { fmt.Println("Before: len(slice) =", len(slice)) newSlice := SubtractOneFromLength(slice) fmt.Println("After: len(slice) =", len(slice)) fmt.Println("After: len(newSlice) =", len(newSlice)) }
Di sini kita lihat bahwa isi dari argumen slice dapat dimodifikasi oleh
sebuah fungsi, namun header-nya tidak.
Panjang yang tersimpan dalam variabel slice
tidak bisa diubah oleh fungsi
yang menerimanya, secara fungsi menerima salinan dari header slice,
bukan yang aslinya.
Sehingga jika kita ingin menulis sebuah fungsi yang memodifikasi header, kita
harus mengembalikan hasilnya, seperti yang kita lakukan di atas.
Variabel slice
tidak berubah namun nilai yang dikembalikan memiliki panjang
yang baru, yang kemudian disimpan ke dalam newSlice
.
Pointer ke slice: method penerima
Cara lain supaya fungsi dapat mengubah header slice yaitu dengan mengirim sebuah pointer. Berikut variasi dari contoh sebelumnya yang melakukan hal tersebut:
func PtrSubtractOneFromLength(slicePtr *[]byte) { slice := *slicePtr *slicePtr = slice[0 : len(slice)-1] } func main() { fmt.Println("Before: len(slice) =", len(slice)) PtrSubtractOneFromLength(&slice) fmt.Println("After: len(slice) =", len(slice)) }
Contoh tersebut tampak janggal, terutama dengan adanya variabel tambahan (sebuah variabel sementara membantu), namun ada satu kasus umum di mana kita dapat menggunakan pointer ke slice. Hal yang idiomatis menggunakan sebuah pointer penerima yaitu pada sebuah method yang memodifikasi sebuah slice.
Katakanlah kita ingin sebuah method pada sebuah slice yang menyingkat isinya sampai slash ("/") yang terakhir. Kita dapat menulisnya seperti ini:
type path []byte func (p *path) TruncateAtFinalSlash() { i := bytes.LastIndex(*p, []byte("/")) if i >= 0 { *p = (*p)[0:i] } } func main() { pathName := path("/usr/bin/tso") // Conversion from string to path. pathName.TruncateAtFinalSlash() fmt.Printf("%s\n", pathName) }
Jika contoh tersebut kita jalankan akan terlihat bahwa ia bekerja dengan benar, mengubah slice dari sisi pemanggil.
(Latihan: Ubah lah tipe dari penerima menjadi sebuah nilai bukan sebuah pointer dan jalankan kembali. Jelaskan apa yang terjadi.)
Di sisi lain, jika kita ingin menulis sebuah method untuk path
yang mengubah
setiap huruf ASCII menjadi huruf besar (anggaplah semuanya menggunakan huruf
latin), method tersebut dapat menggunakan penerima nilai karena penerima
nilai akan tetap menunjuk ke array yang sama.
type path []byte func (p path) ToUpper() { for i, b := range p { if 'a' <= b && b <= 'z' { p[i] = b + 'A' - 'a' } } } func main() { pathName := path("/usr/bin/tso") pathName.ToUpper() fmt.Printf("%s\n", pathName) }
Di sini method ToUpper
menggunakan dua variabel dalam konstruksi for range
untuk mendapatkan indeks dan elemen slice.
Bentuk pengulangan ini menghindari penulisan p[i]
beberapa kali dalam badan
fungsi.
(Latihan: Konversi method ToUpper
menggunakan penerima pointer dan lihat
apakah perilaku fungsi tersebut berubah.)
(Latihan lanjutan: Konversi method ToUpper
supaya dapat menangani huruf
Unicode, bukan hanya ASCII.)
Kapasitas
Lihat fungsi berikut yang mengembangkan argumen slice dari int dengan sebuah elemen:
func Extend(slice []int, element int) []int { n := len(slice) slice = slice[0 : n+1] slice[n] = element return slice }
(Kenapa ia harus mengembalikan slice yang dimodifikasi?) Sekarang jalankan:
func main() { var iBuffer [10]int slice := iBuffer[0:0] for i := 0; i < 20; i++ { slice = Extend(slice, i) fmt.Println(slice) } }
Lihat bagaimana slice tersebut berkembang sampai … berhenti.
Saatnya kita membahas tentang komponen ketiga dari header slice: kapasitas slice. Selain pointer ke array dan panjang, header dari slice juga menyimpan kapasitasnya.
type sliceHeader struct { Length int Capacity int ZerothElement *byte }
Field Capacity
menyimpan berapa banyak ruang dari array;
ia adalah nilai maksimum dari Length
.
Mencoba mengembangkan slice melebihi kapasitasnya akan melangkah keluar dari
limit dari array dan akan menimbulkan panic.
Contoh slice yang dibuat dengan
slice := iBuffer[0:0]
bentuk header-nya seperti berikut:
slice := sliceHeader{ Length: 0, Capacity: 10, ZerothElement: &iBuffer[0], }
Field Capacity
sama dengan panjang dari array, dikurangi indeks dari elemen
pertama array yang ditunjuk oleh slice (dalam kasus ini yaitu nol).
Jika kita ingin mengetahui berapa kapasitas dari sebuah slice, gunakan fungsi
bawaan cap
:
if cap(slice) == len(slice) { fmt.Println("slice is full!") }
Make
Bagaimana bila kita ingin mengembangkan slice melebihi kapasitasnya? Kita tidak bisa! Secara definisi, kapasitas adalah limit pertumbuhan slice. Namun kita dapat mengembangkan slice dengan mengalokasikan sebuah array yang baru, menyalin data, dan memodifikasi slice supaya menggunakan array baru.
Mari mulai dengan alokasi.
Kita dapat menggunakan fungsi bawaan new
untuk mengalokasikan array yang
lebih besar dan kemudian memotong hasilnya, namun akan lebih mudah menggunakan
fungsi bawaan make
.
Fungsi make
mengalokasikan sebuah array baru dan membuat sebuah header
slice.
Fungsi make
menerima tiga argumen: tipe dari slice, panjang awal, dan
kapasitas, yang merupakan panjang array yang dialokasikan oleh make
untuk menyimpan data slice.
Pemanggilan make
berikut membuat sebuah slice dengan panjang 10 dengan sisa
ruang 5 lagi (15-10), seperti yang dapat kita lihat bila menjalankan:
slice := make([]int, 10, 15) fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
Potongan kode berikut melipatgandakan kapasitas slice int
namun tetap
menjaga panjangnya:
slice := make([]int, 10, 15) fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice)) newSlice := make([]int, len(slice), 2*cap(slice)) for i := range slice { newSlice[i] = slice[i] } slice = newSlice fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
Setelah menjalankan kode di atas, slice akan punya banyak ruang untuk tumbuh sebelum butuh alokasi lagi.
Saat membuat slice, terkadang panjang dan kapasitasnya akan sama.
Fungsi make
punya cara singkat untuk kasus umum ini.
Argumen untuk panjang sama dengan kapasitas, sehingga kita dapat
mengindahkan panjang supaya keduanya bernilai sama.
Setelah
gophers := make([]Gopher, 10)
slice gophers
akan memiliki panjang dan kapasitas di set ke 10.
Copy
Saat kita melipatgandakan kapasitas slice pada contoh sebelumnya, kita
menulis sebuah pengulangan untuk menyalin data lama ke slice yang baru.
Go memiliki fungsi bawaan, copy
, untuk mempermudah hal ini.
Argumen dari copy
yaitu dua slice, dan ia menyalin data dari argumen sebelah
kanan ke argumen sebelah kiri.
Berikut penulisan ulang contoh di atas dengan menggunakan dengan copy
:
newSlice := make([]int, len(slice), 2*cap(slice)) copy(newSlice, slice)
Fungsi copy
cukup pintar.
Ia hanya menyalin apa yang ada, memperhatikan panjang dari kedua argumen.
Dengan kata lain, jumlah elemen yang disalin yaitu panjang minimum dari kedua
slice.
Hal ini akan menyingkat beberapa kode.
Fungsi copy
mengembalikan nilai integer, jumlah elemen yang disalin, yang
biasanya jarang diperiksa.
Fungsi copy
juga bekerja dengan baik bila sumber dan tujuan saling timpa,
yang artinya ia dapat digunakan untuk men-shift item dalam sebuah slice.
Berikut cara menggunakan copy
untuk menyisipkan sebuah nilai ke tengah
slice.
// Insert menyisipkan value ke dalam slice indeks tertentu, yang harus berada // dalam rentang. // Argumen slice harus memiliki ruang yang cukup untuk elemen yang baru. func Insert(slice []int, index, value int) []int { // Kembangkan slice dengan satu elemen. slice = slice[0 : len(slice)+1] // Gunakan copy untuk memindahkan bagian atas dari slice dan buka // sebuah ruang. copy(slice[index+1:], slice[index:]) // Simpan value yang baru. slice[index] = value // Kembalikan hasil penyisipan. return slice }
Ada beberapa hal penting yang perlu diperhatikan dalam fungsi di atas. Pertama, ia harus mengembalikan slice yang diubah karena panjangnya telah berubah. Kedua, ia menggunakan cara singkat yang umum. Ekspresi
slice[i:]
sama dengan
slice[i:len(slice)]
Walaupun kita belum menggunakan trik tersebut, kita juga dapat mengindahkan elemen pertama dari ekspresi slice; nilai bawaannya adalah nol. Maka
slice[:]
artinya sama dengan slice itu sendiri, yang berguna saat memotong sebuah array. Ekspresi berikut adalah cara singkat untuk membuat "sebuah slice yang berisi semua elemen dari array":
array[:]
Sekarang sudah jelas, mari kita jalankan fungsi Insert
.
// Ingat kapasitas > panjang: ruang untuk menambahkan elemen. slice := make([]int, 10, 20) for i := range slice { slice[i] = i } fmt.Println(slice) slice = Insert(slice, 5, 99) fmt.Println(slice)
Append: sebuah contoh
Di beberapa bagian sebelumnya, kita menulis fungsi Extend
yang mengembangkan
sebuah slice dengan sebuah elemen.
Fungsi tersebut ada bug-nya, karena bila kapasitas slice terlalu kecil,
fungsi tersebut akan crash.
(Contoh Insert
kita juga punya masalah yang sama.)
Sekarang kita punya bagian pengganti untuk memperbaiki hal tersebut, jadi mari
kita tulis sebuah implementasi dari Extend
untuk slice integer.
func Extend(slice []int, element int) []int { n := len(slice) if n == cap(slice) { // Slice penuh; harus dikembangkan. // Kita lipatgandakan ukurannya dan tambahkan 1, supaya bila // ukurannya 0 masih dapat dikembangkan. newSlice := make([]int, len(slice), 2*len(slice)+1) copy(newSlice, slice) slice = newSlice } slice = slice[0 : n+1] slice[n] = element return slice }
Dalam kasus ini sangat penting untuk mengembalikan slice, karena saat realokasi terjadi, slice yang dihasilkan memiliki array yang berbeda. Berikut potongan kode yang mendemonstrasikan apa yang terjadi saat slice penuh:
slice := make([]int, 0, 5) for i := 0; i < 10; i++ { slice = Extend(slice, i) fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice) fmt.Println("address of 0th element:", &slice[0]) }
Perhatikan realokasi saat inisial array berukuran 5 menjadi penuh. Kapasitas dan alamat dari elemen ke nol berubah saat array yang baru dialokasikan.
Dengan fungsi Extend
sebagai acuan, kita dapat menulis fungsi yang lebih
bagus yang membolehkan kita mengembangkan slice dengan banyak elemen.
Untuk melakukan hal tersebut, kita menggunakan kemampuan Go untuk mengubah
beberapa argumen fungsi menjadi sebuah slice saat fungsi dipanggil.
Yaitu, fasilitas fungsi variadic pada Go.
Katakanlah nama fungsinya Append
.
Untuk versi pertama, kita bisa memanggil Extend
berulang kali supaya
mekanisme dari fungsi variadic cukup jelas.
Penanda dari fungsi Append
yaitu:
func Append(slice []int, items ...int) []int
Fungsi Append
menerima sebuah argumen, sebuah slice, diikuti oleh nol atau
lebih argumen bertipe int
.
Argumen tersebut sebenarnya adalah slice dari int
, seperti yang dapat kita
lihat:
// Append tambahkan item ke slice. // Versi pertama: lakukan pengulangan dengan memanggil Extend. func Append(slice []int, items ...int) []int { for _, item := range items { slice = Extend(slice, item) } return slice }
Perhatikan pengulangan for loop
mengiterasi elemen dari argumen items
,
yang bertipe []int
.
Juga perhatikan penggunakan pengidentifikasi kosong _
untuk mengindahkan
indeks dari pengulangan, yang tidak kita butuhkan dalam kasus ini.
Cobalah:
slice := []int{0, 1, 2, 3, 4} fmt.Println(slice) slice = Append(slice, 5, 6, 7, 8) fmt.Println(slice)
Teknik baru lain dalam contoh tersebut adalah kita dapat menginisiasi slice dengan menulis literal komposit, yang terdiri dari tipe slice diikuti oleh elemennya dalam kurung kurawal:
slice := []int{0, 1, 2, 3, 4}
Fungsi Append
sangat menarik.
Selain dapat menambahkan satu atau beberapa elemen, kita juga dapat
menambahkan sebuah slice dengan "meledakkan" slice menjadi argumen-argumen
menggunakan notasi …
pada saat pemanggilan:
slice1 := []int{0, 1, 2, 3, 4} slice2 := []int{55, 66, 77} fmt.Println(slice1) slice1 = Append(slice1, slice2...) // Sintaksis '...' sangat penting! fmt.Println(slice1)
Tentu saja, kita dapat membuat Append
lebih efisien dengan melakukan alokasi
tidak lebih dari satu kali, membangun berdasarkan dalaman dari Extend
:
// Append tambahkan elemen ke dalam slice. // Versi yang efisien. func Append(slice []int, elements ...int) []int { n := len(slice) total := len(slice) + len(elements) if total > cap(slice) { // Realokasi. Kembangkan 1.5 kali ukuran yang baru, supaya // kita dapat terus tumbuh. newSize := total*3/2 + 1 newSlice := make([]int, total, newSize) copy(newSlice, slice) slice = newSlice } slice = slice[:total] copy(slice[n:], elements) return slice }
Perhatikan bagaimana kita menggunakan copy
dua kali, pertama untuk
memindahkan data slice ke alokasi memori yang baru, dan kemudian untuk
menyalin item-item yang ditambahkan ke akhir dari data lama.
Cobalah; hasilnya sama dengan sebelumnya:
slice1 := []int{0, 1, 2, 3, 4} slice2 := []int{55, 66, 77} fmt.Println(slice1) slice1 = Append(slice1, slice2...) // The '...' is essential! fmt.Println(slice1)
Append: fungsi bawaan
Akhirnya kita sampai pada rancangan dari fungsi bawaan append
.
Perilakunya sama dengan contoh Append
kita, dengan efisiensi yang sama,
namun dapat digunakan untuk semua tipe slice.
Kelemahan Go yaitu operasi yang bersifat generik haruslah disediakan oleh
run-time.
Suatu saat nanti mungkin akan berubah, namun untuk saat sekarang, supaya
bekerja dengan slice lebih mudah, Go menyediakan fungsi bawaan generik
append
.
Ia berlaku sama dengan versi slice int
kita, namun untuk semua tipe slice.
Ingatlah, karena header slice selalu diubah oleh pemanggilan append
, kita
harus menyimpan slice yang dikembalikan setelah pemanggilan.
Pada kenyataannya, compiler tidak membolehkan kita menggunakan append
tanpa menyimpan hasilnya.
Berikut beberapa baris contoh dengan perintah pencetakan. Cobalah, ubah, dan eksplorasi mereka:
// Buat beberapa slice. slice := []int{1, 2, 3} slice2 := []int{55, 66, 77} fmt.Println("Start slice: ", slice) fmt.Println("Start slice2:", slice2) // Tambahkan sebuah item ke slice. slice = append(slice, 4) fmt.Println("Add one item:", slice) // Tambahkan slice ke slice yang lain. slice = append(slice, slice2...) fmt.Println("Add one slice:", slice) // Buat salinan dari slice. slice3 := append([]int(nil), slice...) fmt.Println("Copy a slice:", slice3) // Salin sebuah ke akhir dari dirinya sendiri. fmt.Println("Before append to self:", slice) slice = append(slice, slice...) fmt.Println("After append to self:", slice)
Sangat penting untuk memikirkan mengenai baris terakhir dari contoh di atas dengan lebih rinci supaya paham bagaimana rancangan slice membuat perintah tersebut dapat terjadi dengan pemanggilan yang sederhana dan berjalan dengan benar.
Ada banyak contoh lain dari append
, copy
, dan cara lain untuk menggunakan
slice dalam
halaman wiki Slice Tricks
yang dibangun oleh komunitas.
Nil
Selain itu, dengan pengetahuan yang baru kita dapat mari melihat representasi
dari sebuah slice yang nil
.
Slice yang nil
adalah nilai kosong dari header slice:
sliceHeader{ Length: 0, Capacity: 0, ZerothElement: nil, }
atau hanya
sliceHeader{}
Kuncinya yaitu pointer elemen pada header slice juga nil
.
Slice yang dibuat dengan
array[0:0]
memiliki panjang nol (dan mungkin kapasitas nol) namun pointer-nya tidak
nil
, jadi ia bukanlah slice yang nil
.
Supaya lebih jelas, slice yang kosong dapat berkembang (diasumsikan
kapasitasnya tidak nol), namun slice yang nil
tidak memiliki array tempat
menyimpan nilai dan tidak akan pernah dapat dikembangkan bahkan untuk
menyimpan satu elemen pun.
Sebuah slice yang nil
secara fungsionalitas sama dengan slice dengan panjang
nol, walaupun ia tidak menunjuk ke mana pun.
Ia memiliki panjang nol dan dapat ditambahkan, dengan alokasi.
Sebagai contoh, lihat pernyataan satu baris di atas yang menyalin sebuah slice
dengan menambahkan ke slice nil
.
String
Sekarang sedikit membahas tentang string dalam Go dalam konteks dari slice.
String sebenarnya sangat sederhana: ia adalah slice dari byte yang read-only dengan sedikit dukungan sintaksis ekstra dari bahasa.
Karena sifatnya yang read-only, maka tidak perlu kapasitas (kita tidak bisa mengembangkan string), namun untuk tujuan yang umum kita dapat memperlakukan mereka seperti slice dari byte yang read-only.
Sebagai langkah awal, kita dapat melakukan operasi indeks pada string untuk mengakses byte:
slash := "/usr/ken"[0] // menghasilkan byte dengan nilai '/'.
Kita dapat memotong sebuah string untuk mendapatkan sub-string:
usr := "/usr/ken"[0:4] // menghasilkan string "/usr"
Cukup jelas sekarang apa yang terjadi di belakang saat kita memotong sebuah string.
Kita juga dapat mengubah slice dari byte menjadi string dan membuat sebuah string menjadi slice dari byte dengan konversi sederhana:
str := string(slice)
dan sebaliknya
slice := []byte(usr)
Array di balik sebuah string disembunyikan; kita tidak akan bisa mengakses konten array tersebut kecuali lewat string. Ini artinya saat kita melakukan konversi di atas, salinan dari array harus dibuat. Go tentu saja melakukan semua hal tersebut, jadi Anda tidak perlu khawatir lagi. Setelah konversi, modifikasi terhadap array di belakang slice tidak memengaruhi string yang berkorespondensi.
Konsekuensi penting dari rancangan seperti-slice ini bagi string yaitu membuat operasi sub-string menjadi lebih efisien. Saat sebuah sub-string dibuat yang terjadi adalah dibuatnya dua buah header string. Secara string adalah read-only, string yang asli dan sub-string yang dihasilkan, dari operasi pemotongan, memiliki array yang sama.
Sebuah catatan historis: Implementasi awal dari string selalu membuat alokasi baru, namun saat slice ditambahkan ke dalam bahasa, mereka menyediakan sebuah model untuk penanganan string yang efisien. Beberapa benchmark memperlihatkan peningkatan kecepatan yang besar.
Ada lebih banyak lagi bahasan tentang string, dan sebuah blog terpisah mengover hal tersebut lebih mendalam.
Kesimpulan
Untuk memahami bagaimana slice bekerja, sangatlah membantu untuk memahami bagaimana ia diimplementasikan. Ada struktur data, header slice, yaitu item yang berasosiasi dengan variabel slice, dan header tersebut berisi sebuah bagian dari array yang dialokasikan secara terpisah. Saat kita mengirim nilai slice, header tersebut disalin namun array yang ditunjuk selalu sama.
Saat Anda memahami bagaimana slice bekerja, ia tidak saja menjadi mudah
digunakan, tetapi juga sangat berguna, ekspresif, khususnya dengan bantuan
fungsi bawaan copy
dan append
.
Bacaan lebih lanjut
Ada banyak informasi yang dapat ditemukan di Internet tentang slice dalam Go. Seperti yang disebutkan sebelumnya, halaman Wiki Slice Tricks memiliki banyak contoh dari penggunaan slice. Blog tentang Slice pada Go menjelaskan lebih rinci layout memori dengan diagram yang jelas. Artikel Russ Cox tentang Struktur Data Go berisi diskusi tentang slice berikut dengan beberapa struktur data internal dari Go.
Ada lebih banyak materi lagi yang tersedia, namun cara belajar paling bagus tentang slice yaitu dengan menggunakannya.