Kenapa Saya Pilih Channel daripada Mutex untuk Handle Concurrent Requests

·Abdul Wahid Kahar

Waktu pertama belajar concurrency di Go, instinct pertama saya waktu ada shared state adalah langsung pakai sync.Mutex — karena itu yang paling familiar dari bahasa lain. Tapi setelah implementasi, ada yang terasa tidak beres.

Studi kasusnya begini: saya punya worker yang proses transaksi masuk secara concurrent, dan semua worker perlu akses ke satu map untuk tracking status transaksi.

Versi pertama pakai Mutex:

go
var mu sync.Mutex
status := make(map[string]string)

func updateStatus(id, state string) {
    mu.Lock()
    defer mu.Unlock()
    status[id] = state
}

Kodenya jalan, tapi ada masalah yang tidak langsung kelihatan — setiap goroutine harus nunggu lock dilepas sebelum bisa lanjut. Kalau worker makin banyak, ini jadi bottleneck.

Saya coba ganti pendekatan pakai channel:

go
type statusUpdate struct {
    id    string
    state string
}

updates := make(chan statusUpdate, 100)

go func() {
    status := make(map[string]string)
    for u := range updates {
        status[u.id] = u.state
    }
}()

func updateStatus(id, state string) {
    updates <- statusUpdate{id, state}
}

Sekarang hanya ada satu goroutine yang pegang map-nya — tidak ada yang rebutan akses. Worker tinggal kirim update ke channel dan langsung lanjut kerja.

Trade-off yang perlu dicatat: channel bukan selalu lebih baik dari Mutex. Kalau yang dibutuhkan cuma protect satu variabel sederhana, Mutex lebih straightforward dan lebih mudah dibaca. Channel lebih cocok kalau ada alur data yang perlu dikomunikasikan antar goroutine — bukan sekadar protect shared state.

Go punya prinsip yang bagus untuk ini: "Don't communicate by sharing memory, share memory by communicating." Waktu itu baru benar-benar ngerti maksudnya setelah tulis studi kasus ini sendiri.