Race Condition & Mutual Exclusion di Go

Lu pasti pernah ngalamin kode Go lo bertingkah aneh, muncul bug yang kadang-kadang aja nongol? Bisa jadi lo lagi berhadapan sama race condition. Yuk kita kupas bareng: apa sih itu, kenapa bisa kejadian, dan gimana cara ngatasinnya biar kode lo tetap aman dan nggak ngawur.


Race Condition Itu Apa, Sih? 🤔

Race condition itu kejadian di mana dua atau lebih goroutine (kayak thread ringan) ngakses dan ubah data yang sama di waktu yang barengan. Karena gak ada yang ngatur siapa duluan yang ngakses, hasil akhirnya bisa kacau, kadang data jadi salah, atau yang lebih parah program bisa crash. Bayangin aja dua orang lagi ngedit satu sel di Google Sheet barengan and bisa jadi conflict.


Nih Contohnya:

Misal lo punya counter yang diubah sama banyak goroutine:

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 10000; i++ {
        counter++
    }
}

func TestRaceCondition(t *testing.T) {
    var wg sync.WaitGroup

    wg.Add(10)
    for i := 0; i < 10; i++ {
        go increment(&wg)
    }

    wg.Wait()
    log.Println(counter)
}

Ekspektasinya sih counter bakal jadi 100000, tapi kenyataannya bisa beda. Nah, itu karena race condition!

Output:

go test -v -run=TestRaceCondition
=== RUN   TestRaceCondition
2025/06/18 20:52:21 40579
--- PASS: TestRaceCondition (0.00s)
PASS
ok      race-condition  0.767s

Gimana Tau Kalo Kena Race Condition? 🕵️

Tenang, Go punya tools buat bantuin lo. Tinggal jalanin pake flag -race:

go run -race file.go

Output:

go test -v -race -run=TestRaceCondition
=== RUN   TestRaceCondition
==================
WARNING: DATA RACE
Read at 0x0001008c5530 by goroutine 14:
  race-condition.increment()

Kalau ada race condition, Go bakal kasih tau lokasi masalahnya. Ngebantu banget buat debug.


Solusi

1. Pakai Mutual Exclusion (Mutex)

Mutex itu semacam satpam yang cuma ngasih satu goroutine buat masuk dan ubah data di waktu yang sama.

func TestMutex(t *testing.T) {
	var counter int
	var mu sync.Mutex
	var wg sync.WaitGroup

	for range 10000 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			mu.Lock()
			counter++
			mu.Unlock()
		}()
	}

	wg.Wait()
	log.Println("Counter:", counter)
}
go test -v -run=TestMutex
=== RUN   TestMutex
=== RUN   TestMutex
2025/06/18 21:54:25 Counter: 10000
--- PASS: TestMutex (0.00s)
    --- PASS: TestMutex (0.00s)
PASS
ok      race-condition  0.196s

Dengan mutex, counter bakal stabil di 10000. Jadi nggak saling tabrak antar goroutine.


2. Pakai Channel

Channel itu bikin goroutine bisa komunikasi dengan aman.

counter := 0
ch := make(chan bool)

for i := 0; i < 1000; i++ {
	go func() {
		ch <- true
	}()

	go func() {
		counter++
		<-ch
	}()
}

Tapi jujur buat urusan counter doang, mutex lebih simpel. Channel cocok kalau lo mau ngatur alur yang lebih kompleks antar goroutine.


3. Atomic Operation

Paket sync/atomic di Go bisa dipakai buat operasi thread-safe tanpa perlu mutex.

package main

import (
	"log"
	"sync/atomic"
	"testing"
	"time"
)

var counter int64 = 0

func TestAtomic(t *testing.T) {
	for i := 0; i < 10000; i++ {
		go func() {
			atomic.AddInt64(&counter, 1)
		}()
	}
	time.Sleep(time.Second)
	log.Println("Counter:", counter)
}
go test -v -race -run=TestAtomic
=== RUN   TestAtomic
2025/06/18 22:00:13 Counter: 10000
--- PASS: TestAtomic (1.03s)
PASS
ok      race-condition  2.400s

4. Read-Write Mutex

var rwmu sync.RWMutex

// Banyak yang bisa baca
rwmu.RLock()
value := counter
rwmu.RUnlock()

// Cuma satu yang bisa nulis
rwmu.Lock()
counter++
rwmu.Unlock()

RWMutex cocok kalau kebanyakan proses cuma baca data, karena gak perlu nunggu kayak mutex biasa.


Pro Tips 🎯

  1. Selalu tes pake -race biar bisa deteksi masalah lebih awal:
go test -race ./...
  1. Makin dikit data yang dishare antar goroutine, makin kecil kemungkinan kena race condition.
  2. Setelah mu.Lock(), langsung aja defer mu.Unlock() biar gak lupa buka kunci—bisa-bisa program lo nge-freeze (deadlock).

TL;DR

  • Race condition = bug gak ketebak gara-gara data diakses barengan tanpa kontrol.
  • Kapan pun lo pakai goroutine dan share data, wajib banget mikirin race condition.
  • Pakai -race buat cek dan deteksi.
  • Solusinya: mutex, channel, atau atomic.
  • Kalau bisa, pakai channel atau pattern bawaan Go lain daripada harus ngelock manual.

Kalau lu udah paham konsep race condition dan gimana cara debug + solve sihh gua akuin disitu lu nyampe yee.