โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 2 ยท Concurrency

Data race & detektornya

Data race adalah dua goroutine yang menyentuh memori yang sama tanpa penyelarasan. Ia tidak selalu membuat program crash; sering ia cuma menghasilkan angka yang salah, sesekali.

Sumber asli go.dev Resmi Rangkuman ~6 menit baca

Intisari

  • go test -race ./... di CI adalah gerbang yang tidak boleh dilewati untuk aplikasi concurrent.
  • Detektor hanya melaporkan race yang benar-benar terjadi saat kode dijalankan โ€” jadi cakupan tesmu menentukan cakupan deteksinya.
  • Biaya: 2โ€“20ร— lebih lambat dan memori jauh lebih besar. Untuk tes dan staging, bukan untuk produksi.
  • Setiap laporan race adalah bug nyata. Tidak ada race yang "tidak berbahaya".
  • Penyebab paling umum di aplikasi web: map bersama, field struct yang ditulis dari goroutine latar, dan singleton yang menyimpan data permintaan.

Seperti apa bentuknya

type Counter struct{ n int }

func (c *Counter) Naik() { c.n++ }   // BUKAN operasi tunggal:
                                      // baca n, tambah 1, tulis n

func main() {
	c := &Counter{}
	var wg sync.WaitGroup
	for range 1000 {
		wg.Add(1)
		go func() { defer wg.Done(); c.Naik() }()
	}
	wg.Wait()
	fmt.Println(c.n)   // 987. atau 1000. atau 994. berbeda tiap kali.
}
go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c0000140a0 by goroutine 8:
  main.(*Counter).Naik()
      /app/main.go:6 +0x2c

Previous write at 0x00c0000140a0 by goroutine 7:
  main.(*Counter).Naik()
      /app/main.go:6 +0x44
==================

Laporannya memberi tiga hal: alamat memori yang diperebutkan, dan dua stack trace โ€” siapa membaca dan siapa menulis. Itu biasanya cukup untuk langsung menemukan penyebabnya.

Cara memakainya

go test -race ./...        # โ† ini yang dipasang di CI
go run -race ./cmd/server  # saat mengembangkan
go build -race -o server   # binari untuk staging
LingkunganPakai -race?Alasan
CISelaluSatu-satunya tempat yang menjalankannya secara sistematis
Lokal saat menulis kode concurrentYaUmpan baliknya seketika
Staging bertrafik nyataKadangMenemukan race yang tidak tersentuh tes
ProduksiTidak2โ€“20ร— lebih lambat, memori berlipat

Detektor tidak membuktikan tidak ada race. Ia hanya melaporkan akses yang benar-benar terjadi bersamaan selama eksekusi. Race pada jalur kode yang tidak pernah dijalankan tesmu tidak akan terlihat. Karena itu tes yang menjalankan banyak goroutine sekaligus jauh lebih berharga daripada tes yang memanggil fungsi sekali.

Race yang khas di aplikasi web

1. Map bersama

// โŒ dua permintaan bersamaan = crash "concurrent map writes"
var cache = map[string]Produk{}

func handler(w http.ResponseWriter, r *http.Request) {
	cache[r.URL.Path] = ambilProduk()   // race
}

2. Field yang ditulis goroutine latar

// โŒ konfigurasi yang di-refresh berkala sambil dibaca handler
type Config struct{ Batas int }

var cfg Config

func muatUlangBerkala() {
	for range time.Tick(time.Minute) {
		cfg = ambilConfig()   // race dengan setiap pembaca
	}
}

// โœ… pointer atomik: pembaca tanpa lock, penulis mengganti utuh
var cfg atomic.Pointer[Config]

func muatUlangBerkala() {
	for range time.Tick(time.Minute) {
		c := ambilConfig()
		cfg.Store(&c)
	}
}

func batas() int { return cfg.Load().Batas }

3. Menulis ke ResponseWriter dari dua goroutine

// โŒ http.ResponseWriter tidak aman dipakai bersamaan
go func() { fmt.Fprintln(w, "a") }()
fmt.Fprintln(w, "b")

Semua penulisan respons harus terjadi di goroutine handler. Kalau pekerjaan dibagi, kumpulkan hasilnya lewat channel lalu tulis di satu tempat.

4. time.Time dan slice yang "dibagikan"

Method mengembalikan slice milik struct, pemanggil mengubahnya dari goroutine lain. Ini kembali ke aturan Fase 0: slices.Clone saat mengembalikan slice internal dari tipe yang dipakai bersama.

Tes yang benar-benar menemukan race

func TestCacheParalel(t *testing.T) {
	c := NewCache()
	var wg sync.WaitGroup

	for i := range 100 {
		wg.Add(2)
		go func() { defer wg.Done(); c.Simpan(fmt.Sprint(i), Produk{}) }()
		go func() { defer wg.Done(); c.Ambil(fmt.Sprint(i)) }()
	}
	wg.Wait()
}

Untuk kode berbasis waktu, ada alat baru: paket testing/synctest (eksperimental di Go 1.24, stabil di Go 1.25) menjalankan tes dengan waktu palsu dan mendeteksi saat semua goroutine dalam "gelembung" tes tertidur. Tes yang dulu memakai time.Sleep(100ms) dan tetap flaky bisa jadi deterministik dan selesai seketika. Dibahas lagi di Fase 8.

Setelah menemukan race

  1. Jangan tambal dengan time.Sleep. Itu mengubah peluang, bukan kebenaran.
  2. Tanyakan apakah keadaannya perlu dibagi. Sering jawabannya tidak โ€” beri tiap goroutine salinannya.
  3. Kalau perlu dibagi: mutex untuk struktur data, atomic untuk penghitung, channel untuk penyerahan kepemilikan.
  4. Tulis tes paralel yang gagal dengan -race sebelum memperbaiki, supaya kamu tahu perbaikannya berhasil.

Latihan: jalankan contoh Counter di atas tanpa -race sepuluh kali dan catat hasilnya โ€” perhatikan angkanya berbeda-beda dan program tidak pernah mengeluh. Lalu jalankan dengan -race. Perbaiki dengan atomic.Int64, dan jalankan lagi keduanya.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.