← Semua pembelajaran / Go Nol → Enterprise
Fase 0 · Fondasi Go & Tooling

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.

Sumber asli go.dev Resmi Rangkuman ~8 menit baca

Intisari

  • Slice adalah tampilan ke array: pointer, panjang, kapasitas. Menyalin slice tidak menyalin datanya.
  • append kadang menulis ke array yang sama, kadang mengalokasi yang baru. Kedua perilaku itu benar, dan bedanya bisa menghasilkan bug.
  • Slice dan map bernilai nil tetap bisa dibaca; menulis ke map nil membuat panik.
  • String tidak bisa diubah dan berisi byte, bukan karakter. len("Ω") adalah 2.
  • Paket slices dan maps di 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
TipeSebenarnyaUntuk
stringDeretan byte yang tidak bisa diubahTeks yang dibawa ke mana-mana
byteuint8Data biner, I/O
runeint32 — satu titik kode UnicodeMemproses per karakter
[]byteSlice byte yang bisa diubahMembangun 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.