Mutex & Package sync
Goroutine mudah dibuat, tapi ketika banyak goroutine menyentuh data yang sama, muncul bahaya: race condition. Package sync menyediakan primitif untuk melindungi shared state.
Race Condition: Bukti Masalahnya
var counter int
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // ← TIDAK ATOMIC!
}()
}
wg.Wait()
fmt.Println(counter) // jarang tepat 1000 — biasanya lebih kecil
}
Kenapa? Operasi counter++ sebenarnya tiga langkah: baca → tambah → tulis. Dua goroutine bisa baca nilai yang sama, tambahkan sendiri-sendiri, lalu tulis — satu update hilang.
Jalankan dengan go run -race main.go — Go race detector akan melapor pelanggarannya.
sync.Mutex — Mutual Exclusion
Mutex memastikan hanya satu goroutine yang berada di "critical section" pada satu waktu.
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
defer mu.Unlock() // pola paling umum & aman
counter++
}
Pattern Lock + defer Unlock adalah standar karena:
- Mutex pasti dilepas meski terjadi panic atau early return
- Mudah dibaca — cleanup code berdampingan dengan lock
sync.RWMutex — Read-Write Mutex
Kalau pola akses banyak read, sedikit write, RWMutex lebih cepat: banyak reader bisa lock bersamaan, tapi writer eksklusif.
var (
cache = make(map[string]string)
mu sync.RWMutex
)
func get(k string) string {
mu.RLock() // banyak reader boleh bareng
defer mu.RUnlock()
return cache[k]
}
func set(k, v string) {
mu.Lock() // writer eksklusif — reader pun diblokir
defer mu.Unlock()
cache[k] = v
}
Kapan pakai RWMutex vs Mutex? Jika ratio read:write tinggi (mis. 100:1). Untuk write sering atau ratio seimbang, Mutex biasa biasanya lebih cepat karena overhead RWMutex lebih besar.
sync.Once — Init Sekali Saja
Menjamin fungsi dipanggil tepat sekali, bahkan dari banyak goroutine:
var (
config *Config
once sync.Once
)
func getConfig() *Config {
once.Do(func() {
config = loadConfigFromDisk() // hanya jalan sekali
})
return config
}
Ideal untuk singleton, lazy initialization, atau setup yang mahal.
sync.WaitGroup — Tunggu Banyak Goroutine
Sudah kita lihat di pelajaran Goroutine — recap singkat:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done() // selalu defer agar aman dari panic
doWork(id)
}(i)
}
wg.Wait() // blokir sampai semua Done()
Deadlock — Pitfall Umum
Deadlock terjadi ketika goroutine saling menunggu lock yang tidak akan pernah dilepas. Contoh klasik:
var mu sync.Mutex
func outer() {
mu.Lock()
defer mu.Unlock()
inner() // ← panggil fungsi yang juga Lock()
}
func inner() {
mu.Lock() // ← deadlock! goroutine menunggu dirinya sendiri
defer mu.Unlock()
}
Pencegahan:
- Jangan panggil fungsi lain saat memegang lock, kecuali kamu yakin tidak ada re-lock
- Go tidak punya reentrant mutex — lock dari goroutine yang sama tetap memblokir
- Kalau butuh pecah critical section, pass state-nya sebagai parameter, bukan pegang lock
- Urutan Lock konsisten kalau butuh multiple mutex (hindari dua goroutine lock A→B dan B→A)
sync.Map — Concurrent Map Bawaan
Map alternatif yang aman untuk akses bersamaan tanpa Mutex manual:
var m sync.Map
m.Store("key", 42)
val, ok := m.Load("key") // val = 42 (any), ok = true
m.Delete("key")
m.Range(func(k, v any) bool {
fmt.Println(k, v)
return true // false untuk berhenti
})
Catatan: sync.Map tidak selalu lebih cepat dari Mutex + map. Ia dioptimasi untuk dua pola spesifik: (1) write-once read-many (cache), atau (2) key yang jarang tumpang tindih antar goroutine. Untuk kasus umum, Mutex + map biasanya lebih sederhana dan cepat.
Ringkasan Kapan Pakai Apa
| Kebutuhan | Tool |
|---|---|
| Proteksi shared state umum | sync.Mutex |
| Banyak baca, sedikit tulis | sync.RWMutex |
| Init sekali (singleton) | sync.Once |
| Tunggu N goroutine selesai | sync.WaitGroup |
| Map cache dengan pola khusus | sync.Map |
| Komunikasi data antar goroutine | channel (bukan mutex) |
Motto Go: "Don't communicate by sharing memory; share memory by communicating" — pakai channel untuk aliran data, mutex untuk proteksi state.