Array dan Slice di Go: Struktur Data Paling Dasar
Perbedaan array dan slice Golang sebenarnya sederhana. Array punya ukuran tetap yang ditentukan saat deklarasi dan bersifat value type, artinya setiap assignment menyalin seluruh isinya. Slice ukurannya dinamis dan hanya berisi referensi ke sebuah array di belakangnya, yang biasa disebut backing array. Karena sifat referensi ini, dua slice bisa menunjuk ke data yang sama, dan di situlah banyak bug diam-diam lahir.
Artikel ini bagian keempat dari seri Belajar Struktur Data dari Nol. Kita akan bedah kenapa akses indeks array itu O(1) dilihat dari layout memorinya, praktik operasi dasar beserta analisis kompleksitasnya, sampai latihan reverse dan rotate slice tanpa alokasi baru. Semua contoh dites di Go 1.22, tapi berjalan sama di Go 1.18 ke atas.
Prasyaratnya satu: kamu sudah paham cara membaca kompleksitas algoritma. Kalau notasi O(1) dan O(n) masih terasa asing, baca dulu bagian sebelumnya tentang Big O Notation: Cara Mengukur Kompleksitas Algoritma, karena artikel ini memakai notasi itu terus-menerus.
Array Adalah Blok Memori Berurutan, Karena Itu Aksesnya O(1)
Bayangkan array sebagai deretan loker yang menempel rapat di dinding. Saat kamu menulis var a [5]int64, Go memesan satu blok memori utuh sebesar 40 byte: 5 elemen dikali 8 byte per int64. Elemen-elemennya duduk berdampingan tanpa celah.
Karena elemennya rapat dan ukurannya seragam, alamat elemen ke-i bisa dihitung langsung dengan satu rumus:
alamat elemen i = alamat awal + (i × ukuran elemen)
Mau ambil a[0] atau a[4999], prosesor cuma melakukan satu perkalian dan satu penjumlahan, lalu langsung lompat ke alamat itu. Tidak ada proses menyusuri elemen satu per satu. Inilah alasan akses indeks pada array selalu O(1), tidak peduli arraynya berisi 10 atau 10 juta elemen. Bandingkan dengan linked list yang akan kita bahas di bagian 6 nanti, di mana mencari elemen ke-i harus jalan kaki dari kepala list.
Kita bisa mengintip alamat memorinya langsung:
package main
import "fmt"
func main() {
var a [5]int64
for i := range a {
fmt.Printf("a[%d] di alamat %p\n", i, &a[i])
}
}
Output di mesin saya (alamat awal pasti berbeda di mesinmu, tapi polanya sama):
a[0] di alamat 0xc000012330
a[1] di alamat 0xc000012338
a[2] di alamat 0xc000012340
a[3] di alamat 0xc000012348
a[4] di alamat 0xc000012350
Perhatikan selisih tiap alamat persis 8 byte. Blok memorinya benar-benar berurutan. Efek sampingnya juga bagus untuk performa: CPU cache senang dengan data yang berdekatan, sehingga iterasi array atau slice hampir selalu lebih cepat daripada struktur data berbasis pointer.
Perbedaan Array dan Slice di Go: Ukuran, Semantik, dan Isi Sebenarnya
Di Go, array dan slice adalah dua tipe yang berbeda, bukan sekadar dua nama untuk hal yang sama seperti di beberapa bahasa lain.
| Aspek | Array | Slice |
|---|---|---|
| Deklarasi | var a [5]int |
s := []int{1, 2, 3} |
| Ukuran | Tetap, bagian dari tipe | Dinamis, bisa tumbuh lewat append |
| Semantik assignment | Value type, seluruh isi disalin | Header disalin, data tetap dibagi |
| Isi di memori | Blok data itu sendiri | Pointer, length, capacity |
| Pemakaian umum | Jarang dipakai langsung | Default untuk kumpulan data |
[5]int dan [6]int bahkan dianggap dua tipe berbeda oleh compiler. Fungsi yang menerima [5]int tidak bisa diberi [6]int. Ini yang membuat array mentah jarang muncul di kode Go sehari-hari.
Slice sendiri sebenarnya struct kecil berisi tiga hal: pointer ke backing array, length, dan capacity. Saat kamu mengoper slice ke fungsi, yang disalin cuma struct kecil ini, bukan datanya. Demo berikut memperlihatkan bedanya:
package main
import "fmt"
func main() {
// Array: assignment menyalin seluruh isi
arr1 := [3]int{1, 2, 3}
arr2 := arr1
arr2[0] = 99
fmt.Println("arr1:", arr1) // tidak ikut berubah
fmt.Println("arr2:", arr2)
// Slice: assignment hanya menyalin header
sl1 := []int{1, 2, 3}
sl2 := sl1
sl2[0] = 99
fmt.Println("sl1:", sl1) // ikut berubah
fmt.Println("sl2:", sl2)
}
Output:
arr1: [1 2 3]
arr2: [99 2 3]
sl1: [99 2 3]
sl2: [99 2 3]
arr1 aman karena arr2 adalah salinan penuh. sl1 ikut berubah karena sl1 dan sl2 menunjuk backing array yang sama. Pahami baris ini baik-baik, karena pola yang sama akan muncul lagi di bagian bug nanti.
Operasi Dasar pada Slice dan Analisis Big O Tiap Operasi
Sekarang kita praktikkan empat operasi paling dasar: akses, update, insert di tengah, dan delete di tengah. Ini kombinasi operasi yang di proyek nyata muncul terus, misalnya saat tim Arrazy mengelola daftar item di backend Go untuk sistem aplikasi klien, mayoritas manipulasi datanya berujung pada operasi slice semacam ini.
Akses dan Update: O(1)
s := []string{"nasi", "ayam", "sambal"}
fmt.Println(s[1]) // akses: ayam
s[1] = "bebek" // update
fmt.Println(s[1]) // bebek
Keduanya O(1). Alasannya sama dengan penjelasan layout memori tadi: alamat elemen dihitung langsung dari rumus, tidak ada penelusuran.
Insert di Tengah: O(n)
Slice tidak punya operasi insert bawaan, kita rakit sendiri dari append dan copy:
package main
import "fmt"
func insertAt(s []int, i int, v int) []int {
s = append(s, 0) // tambah satu ruang di ujung
copy(s[i+1:], s[i:]) // geser elemen i sampai akhir, satu langkah ke kanan
s[i] = v // isi lubangnya
return s
}
func main() {
s := []int{10, 20, 40, 50}
s = insertAt(s, 2, 30)
fmt.Println(s)
}
Output:
[10 20 30 40 50]
Delete di Tengah: O(n)
func deleteAt(s []int, i int) []int {
copy(s[i:], s[i+1:]) // geser elemen setelah i, satu langkah ke kiri
return s[:len(s)-1] // potong ekor yang tersisa
}
Panggil deleteAt(s, 2) pada [10 20 30 40 50] dan hasilnya kembali [10 20 40 50]. Sejak Go 1.21 kamu juga bisa memakai slices.Insert dan slices.Delete dari package standar slices, tapi di dalamnya mereka melakukan pergeseran yang sama.
Ringkasan Kompleksitas
| Operasi | Kompleksitas | Alasan |
|---|---|---|
| Akses indeks | O(1) | Alamat dihitung langsung |
| Update indeks | O(1) | Sama, tulis ke alamat hasil hitungan |
| Insert di tengah | O(n) | Semua elemen setelahnya harus digeser |
| Delete di tengah | O(n) | Sama, geser untuk menutup lubang |
| Append di ujung | O(1) amortized | Dibahas tuntas di bagian 5 seri ini |
Kenapa Insert dan Delete di Tengah Itu O(n): Demo Pergeseran Elemen
Bagian ini penting, jadi kita perjelas dengan gambar teks. Ingat, elemen array duduk rapat di memori. Tidak ada ruang kosong di antara mereka. Kalau mau menyisipkan 30 di posisi indeks 2, satu-satunya cara adalah menggeser semua penghuni dari indeks 2 ke kanan dulu:
Sebelum: [10] [20] [40] [50] [ _ ] <- append menambah ruang di ujung
Geser: [10] [20] [40] [40] [50] <- copy(s[3:], s[2:])
^^^^ masih duplikat
Isi: [10] [20] [30] [40] [50] <- s[2] = 30
Dalam kasus terburuk, insert di indeks 0 berarti seluruh n elemen ikut bergeser. Itulah O(n). Delete kebalikannya: elemen di kanan posisi yang dihapus digeser ke kiri untuk menutup lubang.
Konsekuensi praktisnya: kalau programmu sering insert dan delete di posisi acak pada data yang besar, slice mungkin bukan struktur data yang tepat. Kandidat penggantinya, linked list, akan kita bangun sendiri di bagian 6. Tapi jangan buru-buru juga, untuk data berukuran kecil sampai menengah slice sering tetap menang karena ramah CPU cache.
Dua Bug Paling Sering: Index Out of Range dan Backing Array yang Dibagi
Panic Index Out of Range
Ini panic pertama yang hampir semua pemula Go temui:
s := []int{1, 2, 3}
fmt.Println(s[3]) // indeks valid hanya 0, 1, 2
panic: runtime error: index out of range [3] with length 3
Indeks valid selalu 0 sampai len(s)-1. Sumber klasiknya: loop dengan kondisi i <= len(s), mengakses s[len(s)] saat mau ambil elemen terakhir, atau mengakses slice kosong hasil query yang tidak dicek dulu.
Slice yang Berubah Diam-Diam
Bug kedua lebih halus dan lebih berbahaya karena tidak menghasilkan panic. Operasi slicing seperti s[1:3] tidak menyalin data, ia membuat jendela baru ke backing array yang sama:
package main
import "fmt"
func main() {
harga := []int{1000, 2000, 3000, 4000}
promo := harga[1:3] // jendela ke elemen indeks 1 dan 2
promo[0] = 500 // niatnya cuma ubah data promo
fmt.Println("promo:", promo)
fmt.Println("harga:", harga) // ikut berubah tanpa disadari
}
Output:
promo: [500 3000]
harga: [1000 500 3000 4000]
Data harga rusak padahal tidak pernah disentuh langsung. Di aplikasi nyata, bug seperti ini bisa hidup berbulan-bulan sebelum ketahuan. Solusinya: kalau butuh salinan yang benar-benar lepas, salin eksplisit:
promo := make([]int, 2)
copy(promo, harga[1:3])
// atau sejak Go 1.21: promo := slices.Clone(harga[1:3])
Latihan: Reverse dan Rotate Slice In-Place Tanpa Alokasi Baru
Dua fungsi ini soal wawancara kerja yang sangat umum, dan keduanya melatih intuisi manipulasi indeks. Syaratnya: in-place, alias tidak boleh membuat slice baru.
Reverse memakai teknik dua pointer, tukar ujung kiri dan kanan lalu bergerak ke tengah. Rotate memakai trik elegan tiga kali reverse:
package main
import "fmt"
// reverse membalik slice di tempat, O(n) waktu, O(1) memori tambahan
func reverse(s []int) {
for kiri, kanan := 0, len(s)-1; kiri < kanan; kiri, kanan = kiri+1, kanan-1 {
s[kiri], s[kanan] = s[kanan], s[kiri]
}
}
// rotate menggeser semua elemen k posisi ke kanan, in-place
func rotate(s []int, k int) {
if len(s) == 0 {
return
}
k = k % len(s)
reverse(s) // [5 4 3 2 1]
reverse(s[:k]) // [4 5 3 2 1]
reverse(s[k:]) // [4 5 1 2 3]
}
func main() {
a := []int{1, 2, 3, 4, 5}
reverse(a)
fmt.Println("reverse:", a)
b := []int{1, 2, 3, 4, 5}
rotate(b, 2)
fmt.Println("rotate 2:", b)
}
Output:
reverse: [5 4 3 2 1]
rotate 2: [4 5 1 2 3]
Coba telusuri sendiri kenapa tiga reverse menghasilkan rotasi. Tulis kondisi slice setelah tiap langkah di kertas. Kalau kamu bisa menjelaskannya ke orang lain, konsep jendela slice dan manipulasi indeks sudah benar-benar nempel. Perhatikan juga baris k = k % len(s): tanpa itu, rotate(b, 7) pada slice berisi 5 elemen akan panic saat reverse(s[:k]) dipanggil.
Troubleshooting: Error yang Sering Dialami Pemula
panic: runtime error: index out of range [n] with length n
Penyebab: mengakses indeks di luar batas, paling sering s[len(s)]. Solusi: ingat indeks terakhir adalah len(s)-1, dan cek len(s) > 0 sebelum mengakses elemen dari slice yang mungkin kosong. Untuk loop, pakai for i := range s supaya batasnya tidak mungkin salah.
cannot use arr (variable of type [5]int) as []int value
Penyebab: mengoper array ke fungsi yang parameternya slice. Array dan slice adalah tipe berbeda di Go. Solusi: ubah array jadi slice dengan slicing kosong, namaFungsi(arr[:]), atau sejak awal deklarasikan sebagai slice kalau memang tidak butuh ukuran tetap.
Data berubah sendiri padahal tidak pernah diubah langsung
Penyebab: dua slice berbagi backing array yang sama, biasanya hasil operasi slicing atau assignment slice. Perubahan lewat satu slice terlihat di slice lainnya. Solusi: salin eksplisit dengan copy ke slice baru hasil make, atau pakai slices.Clone di Go 1.21 ke atas, sebelum data dimodifikasi.
first argument to append must be a slice; have arr (variable of type [5]int)
Penyebab: memanggil append pada array. Array ukurannya tetap, tidak bisa tumbuh, jadi append hanya menerima slice. Solusi: deklarasikan variabelnya sebagai slice, misalnya s := []int{} alih-alih var s [5]int.
Rangkuman dan Lanjut ke Bagian Berikutnya
Tiga hal untuk dibawa pulang. Pertama, array adalah blok memori berurutan, itulah kenapa akses indeksnya O(1) dan insert di tengahnya O(n). Kedua, perbedaan array dan slice Golang ada di ukuran dan semantik: array tetap dan disalin penuh, slice dinamis dan hanya membawa referensi ke backing array. Ketiga, sifat referensi itu pisau bermata dua, hemat memori tapi rawan bug data berubah diam-diam kalau kamu lupa siapa saja yang memegang backing array yang sama.
Masih ada satu pertanyaan besar yang sengaja belum dijawab: apa yang sebenarnya terjadi saat append dipanggil dan kapasitas slice habis? Kenapa kadang slice hasil append masih terhubung ke slice lama, kadang tidak? Itu jatah bagian kelima, Cara Kerja Slice Go: Append, Kapasitas, dan Memori, yang terbit menyusul. Pantau halaman hub Belajar Struktur Data dari Nol supaya tidak ketinggalan.
Referensi
Artikel Lainnya di Kategori Struktur Data
Struktur Data 8 Agustus 2026
Kompleksitas Algoritma: Kenapa Struktur Data Menentukan Performa
Eksperimen mencari 1 item di 1 juta data membuktikan kenapa struktur data menentukan performa. Lengkap dengan benchmark testing.B dan latihan prediksi.
Baca Artikel
Struktur Data 14 Agustus 2026
Big O Notation: Cara Mengukur Kompleksitas Algoritma
Pahami big O notation dari O(1) sampai O(2^n), lengkap dengan kode Go, benchmark input 1.000 vs 1.000.000, dan cheat sheet semua struktur data.
Baca Artikel
Struktur Data 2 Agustus 2026
Belajar Struktur Data dari Nol: Pengenalan dan Persiapan Go
Mulai belajar struktur data dari nol: konsep dasar, analogi rak gudang, install Go 1.24, setup VS Code, error umum pemula, dan peta 26 bagian seri.
Baca ArtikelIngin Membaca Artikel Lainnya?
Temukan lebih banyak insight dan tips tentang teknologi dan bisnis digital.
Lihat Semua Artikel