Kembali ke Artikel

Array dan Slice di Go: Struktur Data Paling Dasar

Solusi IT

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

BACA JUGA

Artikel Lainnya di Kategori Struktur Data

Ingin Membaca Artikel Lainnya?

Temukan lebih banyak insight dan tips tentang teknologi dan bisnis digital.

Lihat Semua Artikel