Menu

Slice di Golang: append, make, len vs cap, dan Bug Berbagi Array

Slice adalah tipe list sehari-hari di Go. Pelajari make, append dan pertumbuhannya, len vs cap, bagaimana slicing berbagi backing array (dan bug yang ditimbulkannya), copy, package slices, slice 2D, serta nil vs kosong.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Slice dalam satu contoh

Slice adalah tampilan (view) yang bisa membesar ke sebuah array. Kamu membuatnya dengan literal atau dengan make, menambahnya dengan append, dan membaca ukurannya dengan len.

[]string tidak punya panjang di dalam kurung siku. Itulah perbedaannya dengan tipe array seperti [4]string, yang ukurannya tetap.

Apa itu slice

Nilai slice adalah header kecil dengan tiga field:

  • pointer ke sebuah elemen di backing array
  • length: berapa banyak elemen yang bisa dilihat slice itu
  • capacity: berapa banyak elemen yang ada dari pointer itu sampai akhir backing array

Menyalin slice (meng-assign-nya, mengirimkannya ke fungsi) hanya menyalin header ini. Kedua salinan menunjuk ke elemen-elemen yang sama. Hampir setiap kejutan soal slice berasal dari lupa akan hal ini.

make: length dan capacity

make([]T, len, cap) mengalokasikan backing array dan mengembalikan slice di atasnya. Capacity bersifat opsional dan secara default sama dengan length.

Kekeliruan yang umum: make([]int, 5) diikuti lima pemanggilan append menghasilkan sepuluh elemen, lima yang pertama bernilai nol. Pakai make([]int, 0, 5) kalau kamu berencana melakukan append, atau make([]int, 5) kalau kamu berencana meng-assign berdasarkan indeks.

Mengalokasikan capacity lebih dulu saat kamu tahu ukuran akhirnya akan menghindari pertumbuhan berulang. Ini optimasi, bukan keharusan.

append dan pertumbuhan

append menambahkan elemen setelah length saat ini. Kalau capacity-nya cukup, ia menulis di tempat. Kalau tidak, ia mengalokasikan array yang lebih besar, menyalin elemen lama ke sana, lalu mengembalikan slice yang menunjuk ke array baru.

Capacity kira-kira berlipat dua selama slice masih kecil, dan tumbuh dengan faktor yang lebih kecil (mendekati 1,25x) begitu melewati 256 elemen. Angka pastinya adalah detail runtime yang sudah berubah antarversi Go, jadi jangan pernah menulis kode yang bergantung padanya.

Karena append bisa mengembalikan array yang berbeda, kamu harus memakai hasilnya:

append(s, 4)     // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct

Menambahkan seluruh isi slice memakai sintaks spread: s = append(s, other...).

Slicing berbagi backing array

s[low:high] membuat header slice baru di atas array yang sama, mencakup indeks low sampai sebelum high. Batas mana pun boleh dihilangkan. Tidak ada elemen yang disalin.

Output:

[10 99 30 40 50]
3 4

Berbagi array itulah yang membuat slicing murah. Dan itu juga sumber bug slice yang paling terkenal.

Bug aliasing pada append

Saat sebuah sub-slice punya capacity tersisa, append ke sub-slice itu akan menulis ke array milik induknya, menimpa elemen yang masih dipakai induknya.

Output:

[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]

a dan b berbagi slot array setelah "cmd", jadi append yang kedua menimpa yang pertama. Kodenya terlihat baik-baik saja dan berjalan benar setiap kali capacity-nya kebetulan penuh, itulah sebabnya bug ini hanya muncul sesekali.

Dua cara memperbaikinya:

  1. Full slice expression s[low:high:max] membatasi capacity menjadi max-low. Tanpa capacity tersisa, append berikutnya terpaksa mengalokasikan array baru. first := base[:2:2] membuat contoh pertama aman.
  2. Salin secara eksplisit saat sebuah slice akan dipakai lebih lama dari pemanggilannya atau di-append secara terpisah: slices.Clone(prefix) atau append([]string(nil), prefix...).

Jebakan yang berkaitan: slice kecil dari array yang sangat besar membuat seluruh array tetap hidup bagi garbage collector. Kalau kamu membaca file 100 MB lalu menyimpan data[:10], 100 MB itu tetap ada di memori. Clone bagian yang kamu simpan.

copy

Fungsi bawaan copy(dst, src) menyalin min(len(dst), len(src)) elemen dan mengembalikan jumlah itu. Fungsi ini tidak pernah memperbesar dst.

copy menangani sumber dan tujuan yang tumpang tindih dengan benar, jadi copy(s[1:], s) menggeser elemen ke kanan tanpa merusaknya.

Package slices

Sejak Go 1.21, package standar slices punya helper generic untuk operasi yang dulu harus kamu tulis sendiri.

FungsiKegunaannya
Contains, Indexmencari sebuah nilai (ContainsFunc, IndexFunc menerima predicate)
Sort, SortFunc, SortStableFuncmengurutkan di tempat
BinarySearchmencari di slice yang terurut
Insert, Delete, DeleteFuncmenyisipkan atau menghapus, mengembalikan slice baru
Compactmenghapus duplikat yang berurutan
Equal, Comparemembandingkan elemen satu per satu
Clone, Reverse, Max, Minsesuai namanya
Collect, Sorted, Values, Allbekerja dengan iterator (Go 1.23)

Delete dan Insert mengembalikan slice yang harus kamu assign, sama seperti append. Pengurutan punya halamannya sendiri: pengurutan.

Menghapus elemen sambil melakukan perulangan berdasarkan indeks akan melewatkan beberapa elemen. Pakai slices.DeleteFunc sebagai gantinya:

s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens

Slice 2D

Slice berisi slice memberimu grid yang panjang setiap barisnya bisa berbeda. Setiap baris harus dialokasikan secara terpisah.

Lupa melakukan make di bagian dalam membuat setiap baris bernilai nil, dan grid[1][2] = 7 akan panic dengan index out of range [2] with length 0.

Slice nil dan slice kosong

Slice nil berperilaku seperti slice kosong untuk len, cap, range, dan append, jadi pakai var s []T sebagai keadaan nol. Inisialisasi ke []T{} hanya kalau perbedaannya penting, yang kebanyakan terjadi pada output JSON (null vs []). Periksa kekosongan dengan len(s) == 0, bukan s == nil.

Slice dan fungsi

Fungsi yang menerima slice bisa mengubah elemennya, dan pemanggil melihat perubahan itu. Fungsi itu tidak bisa mengubah length milik pemanggil, karena yang diterimanya adalah salinan header. Fungsi yang melakukan append harus mengembalikan slice baru:

func addAll(s []int, vals ...int) []int {
	return append(s, vals...)
}

Inilah sebabnya append, slices.Delete, dan slices.Insert semuanya mengembalikan slice.

Kesalahan umum

  • Mengabaikan hasil append. Tidak bisa dikompilasi kalau dibuang sepenuhnya, tapi append(s, x) yang di-assign ke variabel lain selain yang terus kamu pakai adalah bug logika.
  • Dua append dari satu slice dasar. Keduanya bisa berbagi tempat penyimpanan. Clone slice dasarnya atau pakai full slice expression.
  • make([]T, n) lalu append. Menyisakan n angka nol di bagian depan.
  • Index out of range. s[len(s)] akan panic. Elemen terakhir adalah s[len(s)-1].
  • Membandingkan slice dengan ==. Hanya s == nil yang bisa dikompilasi. Pakai slices.Equal.

Pertanyaan yang sering diajukan

Apa perbedaan length dan capacity sebuah slice di Go?

len(s) adalah jumlah elemen yang saat ini ada di slice. cap(s) adalah jumlah elemen yang muat di backing array, dihitung dari elemen pertama slice itu. append menulis ke kapasitas yang tersisa kalau masih ada, dan mengalokasikan array baru yang lebih besar kalau tidak ada.

Bagaimana cara menambahkan elemen ke slice di Go?

Panggil fungsi bawaan append lalu assign hasilnya kembali: s = append(s, x). Fungsi ini bisa menambahkan beberapa nilai (append(s, 1, 2, 3)) atau slice lain (append(s, other...)). Selalu pakai nilai kembaliannya, karena append bisa mengembalikan slice yang menunjuk ke array baru.

Bagaimana cara menghapus elemen dari slice di Go?

Pakai slices.Delete(s, i, i+1) (Go 1.21), yang menggeser elemen-elemen berikutnya ke bawah dan mengembalikan slice yang lebih pendek. Sejak Go 1.22, fungsi ini juga mengosongkan (zero) slot di ekor yang dibebaskan. Kalau urutan tidak penting, tukar dengan elemen terakhir lalu potong: s[i] = s[len(s)-1]; s = s[:len(s)-1].

Bagaimana cara memeriksa apakah slice berisi sebuah nilai di Go?

Pakai slices.Contains(s, v) dari package standar slices (Go 1.21). slices.Index(s, v) mengembalikan posisinya atau -1, dan slices.ContainsFunc menerima sebuah predicate. Untuk pencarian berulang pada data besar, buat map sebagai gantinya.

Apa perbedaan nil slice dan empty slice di Go?

var s []int bernilai nil; s := []int{} kosong tapi tidak nil. Keduanya punya panjang 0, dan keduanya bisa dipakai dengan len, range, dan append. Perbedaannya muncul pada pemeriksaan s == nil dan di encoding/json, yang meng-encode slice nil sebagai null dan slice kosong sebagai [].

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai