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. Struktur data internal ini serta hubungannya dengan backing array di memori dapat divisualisasikan sebagai berikut:
flowchart TD
subgraph SliceHeader ["Slice Header (Struktur Internal)"]
direction LR
Ptr["Pointer (Alamat Awal Memori)"]
Len["Len (Panjang Slice)"]
Cap["Cap (Kapasitas Slice)"]
end
subgraph BackingArray ["Backing Array di Memori"]
E0["e0"]
E1["e1"]
E2["e2"]
E3["e3"]
E4["e4"]
E5["e5"]
E6["e6"]
E7["e7"]
end
Ptr --> E0
E0 -.->|"Panjang (Len)"| E2
E0 -.->|"Kapasitas (Cap)"| E7
- 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.