Slice, map, string, dan nilai nol
Slice terlihat seperti array yang bisa tumbuh, dan sebagian besar waktu memang bisa dianggap begitu. Sisanya — saat append berbagi memori dengan slice lain — adalah salah satu sumber bug paling halus di Go.
Intisari
- Slice adalah tampilan ke array: pointer, panjang, kapasitas. Menyalin slice tidak menyalin datanya.
appendkadang menulis ke array yang sama, kadang mengalokasi yang baru. Kedua perilaku itu benar, dan bedanya bisa menghasilkan bug.- Slice dan map bernilai
niltetap bisa dibaca; menulis ke map nil membuat panik. - String tidak bisa diubah dan berisi byte, bukan karakter.
len("Ω")adalah 2. - Paket
slicesdanmapsdi pustaka standar menggantikan hampir semua fungsi bantu yang dulu ditulis sendiri.
Slice: tampilan ke array
var kosong []int // nil, len 0, cap 0 — SIAP DIPAKAI
angka := []int{1, 2, 3} // literal
buf := make([]byte, 0, 1024) // len 0, cap 1024 — untuk yang tahu ukurannya
angka = append(angka, 4)
fmt.Println(len(angka), cap(angka)) // 4 6 — kapasitas tumbuh berlipat
potong := angka[1:3] // [2 3] — TIDAK menyalin, menunjuk memori yang sama
Sebuah slice hanyalah tiga angka: alamat awal, panjang, dan kapasitas. Menyalinnya (mengoper ke fungsi, menugaskan ke variabel lain) menyalin tiga angka itu — bukan datanya. Itulah kenapa fungsi yang menerima slice bisa mengubah isi milik pemanggilnya.
Bug klasik nomor satu. append menulis ke array yang sama selama kapasitasnya
masih cukup:
asal := []int{1, 2, 3, 4, 5}
a := asal[:2] // [1 2], tapi cap-nya masih 5
a = append(a, 99) // menimpa asal[2]!
fmt.Println(asal) // [1 2 99 4 5] ← bukan yang kamu maksud
Solusinya satu karakter: full slice expression yang membatasi kapasitas, sehingga
append terpaksa mengalokasi array baru.
a := asal[:2:2] // len 2, cap 2
a = append(a, 99) // aman: array baru, asal tidak tersentuh
Menyalin dengan sengaja
salinan := make([]int, len(asal))
copy(salinan, asal)
salinan := slices.Clone(asal) // Go 1.21+, lebih jelas maksudnya
Kapan ini benar-benar penting: saat sebuah method mengembalikan slice milik struct. Pemanggil bisa mengubah isi struct-mu tanpa melewati satu pun method. Untuk tipe yang dipakai bersama antar goroutine (Fase 2), ini bukan cuma soal kerapian — itu data race.
Map
harga := map[string]int{"kopi": 25000, "teh": 15000}
harga["susu"] = 18000
delete(harga, "teh")
n := harga["tidak ada"] // 0 — nilai nol, BUKAN error
n, ada := harga["tidak ada"] // 0, false — bentuk "comma ok"
if _, ada := harga["kopi"]; ada {
// ada
}
Membaca map nil aman; menulis ke map nil membuat panik. Ini beda
penting dari slice:
var m map[string]int
fmt.Println(m["x"]) // 0 — aman
m["x"] = 1 // panic: assignment to entry in nil map
Struct yang punya field map harus mengisinya di konstruktor, atau field itu jadi ranjau.
Map tidak aman untuk ditulis bersamaan
Dua goroutine yang menulis ke map yang sama akan membuat program crash dengan pesan
concurrent map writes — bukan data rusak diam-diam, tapi mati langsung. Ini disengaja. Solusinya
di Fase 2: sync.Mutex, atau sync.Map untuk pola tulis-sekali-baca-banyak.
String: byte, bukan karakter
s := "Halo, 世界"
len(s) // 13 — jumlah BYTE, bukan huruf
s[0] // 72 — satu byte (tipe byte = uint8), bukan "H"
for i, r := range s { // range pada string menghasilkan RUNE, bukan byte
fmt.Printf("%d: %c\n", i, r) // indeksnya melompat: 0,1,2,3,4,5,6,7,10
}
huruf := []rune(s) // konversi eksplisit
len(huruf) // 9 — jumlah karakter
utf8.RuneCountInString(s) // 9, tanpa mengalokasi slice
| Tipe | Sebenarnya | Untuk |
|---|---|---|
string | Deretan byte yang tidak bisa diubah | Teks yang dibawa ke mana-mana |
byte | uint8 | Data biner, I/O |
rune | int32 — satu titik kode Unicode | Memproses per karakter |
[]byte | Slice byte yang bisa diubah | Membangun teks, memanggil API I/O |
Menyambung string dalam loop adalah jebakan performa. String tidak bisa diubah, jadi
s += x membuat string baru setiap kali. Untuk loop, pakai
strings.Builder:
var b strings.Builder
for _, bagian := range daftar {
b.WriteString(bagian)
}
hasil := b.String()
Nilai nol yang langsung berguna
var b strings.Builder // siap ditulisi
var mu sync.Mutex // siap dikunci
var buf bytes.Buffer // siap dipakai
var wg sync.WaitGroup // siap
var s []int // siap di-append
Ini pola desain yang berulang di seluruh pustaka standar Go: buat nilai nol dari tipemu berguna. Kalau sebuah tipe butuh konstruktor untuk bisa dipakai sama sekali, itu keputusan yang harus disadari — dan biasanya berarti tipe itu memang punya dependensi wajib (koneksi, konfigurasi), yang justru bagus kalau dinyatakan lewat konstruktor.
slices dan maps: berhenti menulis fungsi bantu
import (
"maps"
"slices"
)
slices.Contains(daftar, "kopi")
slices.Index(daftar, "kopi")
slices.Sort(angka)
slices.SortFunc(produk, func(a, b Produk) int { return cmp.Compare(a.Harga, b.Harga) })
slices.Reverse(daftar)
slices.Max(angka)
slices.Equal(a, b)
slices.Clone(daftar)
kunci := slices.Sorted(maps.Keys(harga)) // urut, deterministik
nilai := slices.Collect(maps.Values(harga))
Kalau kamu menemukan paket util berisi ContainsString,
UniqueInts, dan kawan-kawan di kode Go, hampir pasti kode itu ditulis sebelum Go 1.21.
go fix (materi sebelumnya) sekarang menuliskan sebagian penggantinya untukmu.
Latihan: ketik ulang contoh asal[:2] di atas dan buktikan sendiri bahwa
asal ikut berubah. Lalu ganti jadi asal[:2:2] dan buktikan tidak. Terakhir,
tulis fungsi yang menerima []string, mengurutkannya dengan slices.Sort, dan
tidak mengubah slice milik pemanggil — lalu tulis satu tes yang membuktikannya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.