Slice #
Slice adalah tipe data yang paling sering kamu gunakan di Go — lebih dari array, lebih dari map. Hampir semua koleksi data di Go diekspresikan sebagai slice. Tapi slice bukan sekadar “array yang bisa resize.” Di baliknya ada mekanisme tiga-komponen yang perlu kamu pahami untuk menghindari bug paling halus dan paling sering terjadi di Go: shared backing array. Artikel ini membahas slice dari cara kerjanya di memori hingga semua operasi idiomatik yang dipakai di kode produksi.
Anatomi Slice — Tiga Komponen Internal #
Setiap variabel slice di Go menyimpan tiga field dalam sebuah struct kecil yang disebut slice header:
Slice Header:
┌─────────────┬─────┬─────┐
│ Pointer │ Len │ Cap │
└─────────────┴─────┴─────┘
│
▼
Backing Array: [ e0 | e1 | e2 | e3 | e4 | e5 | e6 | e7 ]
↑ ↑
pointer pointer + cap
|←── len ──→|
|←────── cap ────────→|
- Pointer — menunjuk ke elemen pertama yang “terlihat” oleh slice ini di dalam backing array
- Len (length) — jumlah elemen yang saat ini ada dalam slice; yang bisa kamu akses dengan index
- Cap (capacity) — jumlah elemen yang tersedia dari posisi pointer sampai akhir backing array
s := []int{10, 20, 30, 40, 50}
fmt.Println(len(s)) // 5 — panjang
fmt.Println(cap(s)) // 5 — kapasitas = panjang backing array dari pointer
// Sub-slice memindahkan pointer, len berubah, cap berkurang
sub := s[1:3] // {20, 30}
fmt.Println(len(sub)) // 2
fmt.Println(cap(sub)) // 4 — dari index 1 sampai akhir backing array (5-1=4)
Cara Membuat Slice #
Literal #
// Paling umum untuk data yang sudah diketahui
angka := []int{1, 2, 3, 4, 5}
nama := []string{"Budi", "Sari", "Ahmad"}
kosong := []int{} // empty slice — bukan nil
make([]T, len, cap) — Pre-alokasi
#
Gunakan make ketika panjang atau kapasitas akhir sudah bisa diperkirakan:
// Slice dengan len=5, semua elemen zero value
s1 := make([]int, 5) // len=5, cap=5
fmt.Println(s1) // [0 0 0 0 0]
// Slice dengan len=0 tapi cap=100 — siap ditambah sampai 100 elemen tanpa re-alokasi
s2 := make([]int, 0, 100) // len=0, cap=100
fmt.Println(len(s2), cap(s2)) // 0 100
// Berguna ketika tahu berapa elemen akan ditambah
hasil := make([]int, 0, len(input))
for _, v := range input {
if v > 0 {
hasil = append(hasil, v)
}
}
Dari Array #
arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4] // {20, 30, 40} — berbagi backing array dengan arr
Nil Slice vs Empty Slice #
var nilSlice []int // nil slice — pointer=nil, len=0, cap=0
emptySlice := []int{} // empty slice — pointer non-nil, len=0, cap=0
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
// Keduanya punya len=0 dan bisa di-append
fmt.Println(len(nilSlice)) // 0
fmt.Println(len(emptySlice)) // 0
// PERBEDAAN KRITIS: serialisasi JSON
import "encoding/json"
data1, _ := json.Marshal(nilSlice) // null
data2, _ := json.Marshal(emptySlice) // []
fmt.Println(string(data1)) // null
fmt.Println(string(data2)) // []
Nil slice vs empty slice menghasilkan JSON yang berbeda. Jika API kamu perlu mengembalikan array kosong (bukannull), pastikan menggunakan[]T{}ataumake([]T, 0), bukanvar s []T. Klien JavaScript yang menerimanullalih-alih[]sering mengalami error karenanull.map()tidak valid.
Slicing Expression #
Kamu bisa mengambil potongan dari slice atau array menggunakan sintaks s[low:high]:
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] // {2, 3, 4} — dari index 2 sampai 4 (tidak termasuk 5)
s2 := s[:3] // {0, 1, 2} — dari awal sampai index 2
s3 := s[7:] // {7, 8, 9} — dari index 7 sampai akhir
s4 := s[:] // {0,...,9} — seluruh slice (header baru, backing array sama)
// Aturan: 0 <= low <= high <= cap(s)
Three-Index Slice — Batasi Kapasitas #
Three-index slice s[low:high:max] memungkinkan kamu mengontrol kapasitas slice hasil:
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Two-index: cap mengikuti sisa backing array
s1 := s[2:5] // len=3, cap=8 (dari index 2 sampai akhir)
// Three-index: cap dibatasi
s2 := s[2:5:6] // len=3, cap=4 (dari index 2, max index 6)
fmt.Println(len(s1), cap(s1)) // 3 8
fmt.Println(len(s2), cap(s2)) // 3 4
// Kenapa berguna? Append ke s2 tidak akan "merusak" elemen s setelah index 6
Shared Backing Array — Gotcha Terpenting #
Ini adalah sumber bug yang paling umum dengan slice. Ketika kamu melakukan slicing, slice baru berbagi backing array yang sama dengan slice asli:
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // {2, 3}
// Modifikasi sub MENGUBAH original!
sub[0] = 999
fmt.Println(original) // [1 999 3 4 5] — berubah!
fmt.Println(sub) // [999 3]
// Dan sebaliknya
original[2] = 777
fmt.Println(sub) // [999 777] — sub juga berubah!
Kapan Ini Menjadi Bug #
// ANTI-PATTERN: fungsi yang memodifikasi slice dari luar tanpa disadari
func processFirst3(data []int) []int {
result := data[:3]
result[0] = 0 // MODIFIKASI data ASLI! Caller tidak mengharapkan ini
return result
}
// BENAR: buat salinan independen
func processFirst3Safe(data []int) []int {
if len(data) < 3 {
return nil
}
result := make([]int, 3)
copy(result, data[:3]) // copy membuat backing array baru
result[0] = 0 // hanya memodifikasi result, bukan data asli
return result
}
append — Cara Kerja dan Gotcha
#
append menambahkan elemen ke slice dan mengembalikan slice baru:
s := []int{1, 2, 3}
s = append(s, 4) // tambah satu elemen
s = append(s, 5, 6, 7) // tambah beberapa elemen sekaligus
// Spread operator — gabungkan dua slice
a := []int{1, 2, 3}
b := []int{4, 5, 6}
c := append(a, b...)
fmt.Println(c) // [1 2 3 4 5 6]
Kapan append Mengalokasikan Backing Array Baru #
Ini penting dipahami:
s := make([]int, 3, 5) // len=3, cap=5
fmt.Printf("ptr=%p, len=%d, cap=%d\n", &s[0], len(s), cap(s))
s = append(s, 4) // masih ada ruang (cap=5, len sekarang 4)
fmt.Printf("ptr=%p, len=%d, cap=%d\n", &s[0], len(s), cap(s))
// ptr SAMA — backing array tidak berubah!
s = append(s, 5) // penuh! (cap=5, len sekarang 5)
s = append(s, 6) // melebihi cap → alokasi backing array BARU (cap ≈ 2x)
fmt.Printf("ptr=%p, len=%d, cap=%d\n", &s[0], len(s), cap(s))
// ptr BERBEDA — backing array baru dengan cap ≈ 10
Gotcha: append Tidak Selalu Mempertahankan Sharing #
a := []int{1, 2, 3, 4, 5}
b := a[:3] // b berbagi backing array dengan a
c := a[:3] // c juga berbagi backing array yang sama
// Append ke b ketika masih ada cap
b = append(b, 99) // cap masih cukup → memodifikasi backing array a!
fmt.Println(a) // [1 2 3 99 5] — a berubah!
fmt.Println(b) // [1 2 3 99]
fmt.Println(c) // [1 2 3] — c masih [1 2 3] (len=3, tidak "melihat" elemen ke-4)
// Append ke b lagi setelah cap penuh
b = append(b, 88, 77, 66) // melebihi cap → backing array BARU
b[0] = 0 // sekarang tidak mempengaruhi a
fmt.Println(a) // [1 2 3 99 5] — a tidak berubah
Selalu simpan hasil
appendke variabel.appendmungkin mengembalikan slice dengan backing array yang berbeda dari input. Jika kamu tidak menyimpan hasilnya, semua penambahan hilang.// ANTI-PATTERN: hasil append diabaikan func addItem(s []int, item int) { append(s, item) // ✗ hasil dibuang! tidak ada efek } // BENAR: kembalikan slice baru func addItem(s []int, item int) []int { return append(s, item) // ✓ }
copy — Membuat Slice Independen
#
copy(dst, src) menyalin elemen dari src ke dst dan mengembalikan jumlah elemen yang disalin (minimum dari len(dst) dan len(src)):
src := []int{1, 2, 3, 4, 5}
// Salinan penuh
dst := make([]int, len(src))
n := copy(dst, src)
fmt.Println(dst, n) // [1 2 3 4 5] 5
// Modifikasi dst tidak mempengaruhi src
dst[0] = 999
fmt.Println(src) // [1 2 3 4 5] — tidak berubah!
// Salinan parsial — copy mengambil minimum dari len(dst) dan len(src)
partial := make([]int, 3)
copy(partial, src) // hanya 3 elemen yang disalin
fmt.Println(partial) // [1 2 3]
// Copy antar posisi dalam slice yang sama (overlap aman)
s := []int{1, 2, 3, 4, 5}
copy(s[1:], s[0:]) // geser semua elemen ke kanan satu posisi
fmt.Println(s) // [1 1 2 3 4]
Operasi Idiomatik #
Delete Elemen #
s := []int{1, 2, 3, 4, 5}
// Hapus elemen di index i (tidak mempertahankan urutan — lebih cepat)
func deleteUnordered(s []int, i int) []int {
s[i] = s[len(s)-1] // pindahkan elemen terakhir ke posisi i
return s[:len(s)-1] // kurangi panjang
}
// Hapus elemen di index i (mempertahankan urutan)
func deleteOrdered(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// Contoh
s = deleteOrdered(s, 2)
fmt.Println(s) // [1 2 4 5]
Insert Elemen #
// Insert nilai v di index i
func insert(s []int, i int, v int) []int {
s = append(s, 0) // tambah ruang di akhir
copy(s[i+1:], s[i:]) // geser elemen ke kanan
s[i] = v // isi posisi i
return s
}
s := []int{1, 2, 4, 5}
s = insert(s, 2, 3) // insert 3 di index 2
fmt.Println(s) // [1 2 3 4 5]
Filter — Ambil yang Memenuhi Kondisi #
// Filter in-place — reuse backing array, lebih efisien memori
func filter(s []int, keep func(int) bool) []int {
result := s[:0] // slice dengan len=0, cap=cap(s), backing array sama
for _, v := range s {
if keep(v) {
result = append(result, v)
}
}
return result
}
angka := []int{1, -2, 3, -4, 5, -6}
positif := filter(angka, func(n int) bool { return n > 0 })
fmt.Println(positif) // [1 3 5]
Deduplicate — Hapus Duplikat #
func deduplicate(s []int) []int {
if len(s) == 0 {
return s
}
seen := make(map[int]bool)
result := s[:0]
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
data := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}
fmt.Println(deduplicate(data)) // [3 1 4 5 9 2 6]
Reverse #
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
s := []int{1, 2, 3, 4, 5}
reverse(s)
fmt.Println(s) // [5 4 3 2 1]
Sorting Slice #
Package sort menyediakan fungsi untuk mengurutkan slice:
import "sort"
// Integer
angka := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(angka)
fmt.Println(angka) // [1 1 2 3 4 5 6 9]
// String
kata := []string{"banana", "apple", "cherry", "date"}
sort.Strings(kata)
fmt.Println(kata) // [apple banana cherry date]
// Float
nilai := []float64{3.14, 1.41, 2.71, 1.73}
sort.Float64s(nilai)
fmt.Println(nilai) // [1.41 1.73 2.71 3.14]
// Custom — sort.Slice dengan fungsi less
type Person struct {
Name string
Age int
}
orang := []Person{
{"Charlie", 30},
{"Alice", 25},
{"Bob", 35},
}
// Sort by name
sort.Slice(orang, func(i, j int) bool {
return orang[i].Name < orang[j].Name
})
fmt.Println(orang) // [{Alice 25} {Bob 35} {Charlie 30}]
// Sort by age (descending)
sort.Slice(orang, func(i, j int) bool {
return orang[i].Age > orang[j].Age // > untuk descending
})
fmt.Println(orang) // [{Bob 35} {Charlie 30} {Alice 25}]
// Cek apakah sudah terurut
fmt.Println(sort.IntsAreSorted([]int{1, 2, 3, 4})) // true
fmt.Println(sort.IntsAreSorted([]int{1, 3, 2, 4})) // false
// Binary search pada slice yang sudah terurut
sort.Ints(angka)
i, found := sort.Find(len(angka), func(i int) int {
return angka[i] - 5 // cari 5
})
fmt.Println(i, found) // index dan apakah ditemukan
Pre-alokasi untuk Performa #
Re-alokasi backing array saat append melebihi kapasitas adalah operasi mahal (alokasi memori baru + copy semua elemen). Jika kamu tahu berapa elemen yang akan ada, pre-alokasi dengan make menghindari re-alokasi berulang:
// ANTI-PATTERN: re-alokasi berulang
func buildSliceSlow(n int) []int {
var result []int // cap=0
for i := 0; i < n; i++ {
result = append(result, i) // re-alokasi ~log(n) kali!
}
return result
}
// BENAR: pre-alokasi sekali
func buildSliceFast(n int) []int {
result := make([]int, 0, n) // cap=n dari awal
for i := 0; i < n; i++ {
result = append(result, i) // tidak pernah re-alokasi
}
return result
}
// Atau jika panjang final sama dengan kapasitas:
func buildSliceDirect(n int) []int {
result := make([]int, n) // len=n, semua zero
for i := range result {
result[i] = i // assign langsung, tidak butuh append
}
return result
}
Contoh Program Lengkap #
Program berikut membangun sistem inventaris sederhana yang menggunakan berbagai operasi slice:
package main
import (
"fmt"
"sort"
"strings"
)
type Product struct {
ID int
Name string
Category string
Price float64
Stock int
}
type Inventory struct {
products []Product
nextID int
}
func NewInventory() *Inventory {
return &Inventory{
products: make([]Product, 0, 16), // pre-alokasi
}
}
func (inv *Inventory) Add(name, category string, price float64, stock int) {
inv.nextID++
inv.products = append(inv.products, Product{
ID: inv.nextID,
Name: name,
Category: category,
Price: price,
Stock: stock,
})
}
// Filter produk berdasarkan kriteria
func (inv *Inventory) Filter(keep func(Product) bool) []Product {
result := make([]Product, 0)
for _, p := range inv.products {
if keep(p) {
result = append(result, p)
}
}
return result
}
// Hapus produk berdasarkan ID
func (inv *Inventory) Remove(id int) bool {
for i, p := range inv.products {
if p.ID == id {
// Hapus dengan mempertahankan urutan
inv.products = append(inv.products[:i], inv.products[i+1:]...)
return true
}
}
return false
}
// Update stok
func (inv *Inventory) UpdateStock(id, delta int) error {
for i := range inv.products {
if inv.products[i].ID == id {
newStock := inv.products[i].Stock + delta
if newStock < 0 {
return fmt.Errorf("stok tidak cukup: tersedia %d, dikurangi %d",
inv.products[i].Stock, -delta)
}
inv.products[i].Stock = newStock
return nil
}
}
return fmt.Errorf("produk ID %d tidak ditemukan", id)
}
// Ambil semua kategori unik
func (inv *Inventory) Categories() []string {
seen := make(map[string]bool)
var cats []string
for _, p := range inv.products {
if !seen[p.Category] {
seen[p.Category] = true
cats = append(cats, p.Category)
}
}
sort.Strings(cats)
return cats
}
// Sort produk berdasarkan field tertentu
func (inv *Inventory) SortBy(field string, ascending bool) {
sort.Slice(inv.products, func(i, j int) bool {
a, b := inv.products[i], inv.products[j]
var less bool
switch field {
case "name":
less = a.Name < b.Name
case "price":
less = a.Price < b.Price
case "stock":
less = a.Stock < b.Stock
default:
less = a.ID < b.ID
}
if ascending {
return less
}
return !less
})
}
// Laporan ringkasan
func (inv *Inventory) Summary() {
if len(inv.products) == 0 {
fmt.Println("Inventaris kosong")
return
}
// Hitung statistik menggunakan slice operations
totalValue := 0.0
lowStock := inv.Filter(func(p Product) bool { return p.Stock < 5 })
outOfStock := inv.Filter(func(p Product) bool { return p.Stock == 0 })
for _, p := range inv.products {
totalValue += p.Price * float64(p.Stock)
}
fmt.Printf("Total produk : %d\n", len(inv.products))
fmt.Printf("Total nilai stok : Rp%.0f\n", totalValue)
fmt.Printf("Stok menipis (<5) : %d produk\n", len(lowStock))
fmt.Printf("Habis : %d produk\n", len(outOfStock))
fmt.Printf("Kategori : %s\n", strings.Join(inv.Categories(), ", "))
}
// Cetak tabel produk
func printProducts(products []Product, title string) {
if len(products) == 0 {
fmt.Printf("\n%s: (kosong)\n", title)
return
}
fmt.Printf("\n%s:\n", title)
fmt.Printf(" %-4s %-20s %-12s %10s %6s\n",
"ID", "Nama", "Kategori", "Harga", "Stok")
fmt.Println(" " + strings.Repeat("-", 58))
for _, p := range products {
fmt.Printf(" %-4d %-20s %-12s %10.0f %6d\n",
p.ID, p.Name, p.Category, p.Price, p.Stock)
}
}
func main() {
inv := NewInventory()
// Tambah produk
inv.Add("Laptop Pro 14", "Elektronik", 15_000_000, 10)
inv.Add("Mouse Wireless", "Elektronik", 350_000, 3)
inv.Add("Keyboard Mech", "Elektronik", 1_500_000, 7)
inv.Add("Monitor 27\"", "Elektronik", 5_000_000, 2)
inv.Add("Kaos Polos", "Fashion", 85_000, 50)
inv.Add("Celana Chino", "Fashion", 250_000, 30)
inv.Add("Jaket Bomber", "Fashion", 450_000, 4)
inv.Add("Buku Go Bahasa", "Buku", 180_000, 15)
inv.Add("Buku Clean Code", "Buku", 220_000, 0)
// Tampilkan semua produk
printProducts(inv.products, "Semua Produk (urutan tambah)")
// Sort berdasarkan harga — ascending
inv.SortBy("price", true)
printProducts(inv.products, "Urut Harga (murah ke mahal)")
// Filter — hanya elektronik
elektronik := inv.Filter(func(p Product) bool {
return p.Category == "Elektronik"
})
printProducts(elektronik, "Produk Elektronik")
// Filter — stok menipis
menipis := inv.Filter(func(p Product) bool {
return p.Stock > 0 && p.Stock < 5
})
printProducts(menipis, "Stok Menipis (1-4 unit)")
// Slice operations
fmt.Println("\n=== Operasi Slice ===")
// Ambil 3 produk termahal — sort descending dulu
inv.SortBy("price", false)
top3 := inv.products[:3] // slicing — berbagi backing array!
fmt.Println("3 Produk Termahal:")
for i, p := range top3 {
fmt.Printf(" %d. %s — Rp%.0f\n", i+1, p.Name, p.Price)
}
// Buat salinan independen untuk modifikasi aman
top3Copy := make([]Product, len(top3))
copy(top3Copy, top3)
top3Copy[0].Price = 0 // hanya mengubah salinan, bukan inv.products!
fmt.Printf("Harga asli setelah modifikasi salinan: Rp%.0f\n",
inv.products[0].Price) // tidak berubah
// Update stok
fmt.Println("\n=== Update Stok ===")
if err := inv.UpdateStock(1, -3); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Berhasil kurangi stok Laptop Pro 14")
}
// Coba kurangi stok lebih dari yang ada
if err := inv.UpdateStock(2, -10); err != nil {
fmt.Println("Error:", err)
}
// Hapus produk
removed := inv.Remove(9) // hapus "Buku Clean Code" yang stok 0
fmt.Printf("\nHapus produk ID 9: %v\n", removed)
// Ringkasan akhir
fmt.Println("\n=== Ringkasan Inventaris ===")
inv.Summary()
}
Ringkasan #
- Slice header tiga komponen: pointer, len, cap — memahami ini adalah kunci memahami semua perilaku slice.
- Nil slice vs empty slice:
var s []T(nil, JSONnull) vss := []T{}(empty, JSON[]) — berbeda untuk serialisasi.- Slicing berbagi backing array — modifikasi sub-slice mempengaruhi slice asli; gunakan
copyuntuk slice independen.appendselalu reassign:s = append(s, v)— janganappend(s, v)tanpa menyimpan hasilnya.appendbisa alokasi backing array baru — saat melebihi cap; setelah itu sub-slice lama tidak lagi berbagi memori.copy(dst, src)menyalinmin(len(dst), len(src))elemen — selalu buat dst denganmakeyang cukup besar.- Pre-alokasi dengan
make([]T, 0, n)ketika jumlah elemen bisa diperkirakan — hindari re-alokasi berulang.sort.Slicedengan fungsi less kustom untuk sorting struct berdasarkan field apapun.- Filter in-place dengan
result := s[:0]— reuse backing array tanpa alokasi baru.- Three-index slice
s[low:high:max]untuk mengontrol cap slice hasil dan mencegah append tidak sengaja memodifikasi elemen di luar rentang.