Mutex & sync package — Go

Mutex & Package sync Goroutine mudah dibuat, tapi ketika banyak goroutine menyentuh data yang sama, muncul bahaya: race condition. Package sync menyediakan prim

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:

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:

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

KebutuhanTool
Proteksi shared state umumsync.Mutex
Banyak baca, sedikit tulissync.RWMutex
Init sekali (singleton)sync.Once
Tunggu N goroutine selesaisync.WaitGroup
Map cache dengan pola khusussync.Map
Komunikasi data antar goroutinechannel (bukan mutex)

Motto Go: "Don't communicate by sharing memory; share memory by communicating" — pakai channel untuk aliran data, mutex untuk proteksi state.

Yang akan kamu pelajari