Data race adalah bug paling kejam di program concurrent: program tampak sehat di pengujian, lalu menghasilkan angka yang salah — atau crash — hanya pada beban tinggi di production. Race detector Go menutup celah itu dengan mengompilasi ulang program menjadi versi yang mencatat setiap akses memori dan membandingkan pasangan yang bertentangan saat program berjalan. Materi dasar data race — definisi, tiga syaratnya, dan perbandingan perbaikan mutex/atomic — sudah dibahas tuntas pada bab Race Condition ; chapter ini fokus pada bagian yang belum: membaca laporan race detector baris demi baris, dan menyusun alur diagnosis yang bisa diulang sampai program benar-benar bersih.

Menjalankan Program dengan Flag -race #

Berkas contoh-race.go di bawah halaman memakai pola klasik: dua goroutine menambah variabel saldo yang sama 1000 kali tanpa sinkronisasi. Program sengaja tidak mencetak nilai saldo — nilainya berubah-ubah antar eksekusi, dan justru itulah gejalanya:

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	saldo := 0 // variabel bersama: dua goroutine menulis tanpa proteksi

	wg.Add(2)
	go func() {
		defer wg.Done()
		for range 1000 {
			saldo++ // baca, tambah, tulis — tanpa sinkronisasi
		}
	}()
	go func() {
		defer wg.Done()
		for range 1000 {
			saldo++ // akses yang sama persis ke memori yang sama
		}
	}()

	wg.Wait()
	// Nilai saldo sengaja tidak dicetak: nilainya berubah-ubah
	// antar eksekusi (2000 atau kurang) — itulah gejala data race.
	fmt.Println("transaksi selesai")
}

Output yang Dihasilkan:

text
transaksi selesai

Eksekusi biasa keluar 0 dan tampak normal — bahaya inilah yang membuat race lolos dari pengujian manual. Sekarang jalankan berkas yang sama dengan flag -race:

bash
go run -race contoh-race.go

Output yang Dihasilkan:

text
==================
WARNING: DATA RACE
Write at 0x00c000094038 by goroutine 9:
  main.main.func2()
      /home/z/my-project/content/go/debugging-dan-profiling/race-detector/contoh-race.go:22 +0xab

Previous write at 0x00c000094038 by goroutine 8:
  main.main.func1()
      /home/z/my-project/content/go/debugging-dan-profiling/race-detector/contoh-race.go:16 +0xab

Goroutine 9 (running) created at:
  main.main()
      /home/z/my-project/content/go/debugging-dan-profiling/race-detector/contoh-race.go:19 +0x1c8

Goroutine 8 (finished) created at:
  main.main()
      /home/z/my-project/content/go/debugging-dan-profiling/race-detector/contoh-race.go:13 +0x126
==================
==================
WARNING: DATA RACE
Read at 0x00c000094038 by goroutine 9:
  main.main.func2()
      /home/z/my-project/content/go/debugging-dan-profiling/race-detector/contoh-race.go:22 +0x99

Previous write at 0x00c000094038 by goroutine 8:
  main.main.func1()
      /home/z/my-project/content/go/debugging-dan-profiling/race-detector/contoh-race.go:16 +0xab

Goroutine 9 (running) created at:
  main.main()
      /home/z/my-project/content/go/debugging-dan-profiling/race-detector/contoh-race.go:19 +0x1c8

Goroutine 8 (finished) created at:
  main.main()
      /home/z/my-project/content/go/debugging-dan-profiling/race-detector/contoh-race.go:13 +0x126
==================
Found 2 data race(s)
exit status 66

Membaca Laporan Race #

Setiap blok WARNING: DATA RACE bercerita lengkap dalam empat bagian. Baris Write at ... by goroutine 9 adalah akses yang ketahuan terjadi bersamaan, lengkap dengan stack trace sampai baris sumber (contoh-race.go:22). Baris Previous write ... by goroutine 8 adalah lawan bentroknya — akses sebelumnya ke alamat yang sama tanpa hubungan happens-before. Dua blok created at menunjukkan titik lahir kedua goroutine (baris 19 dan 13), sehingga kita bisa melompat langsung ke kode yang memulai kerusuhan. Pada contoh ini detector bahkan melaporkan dua race: pasangan tulis–tulis dan pasangan baca–tulis, keduanya pada baris saldo++ di kedua goroutine — operasi “tunggal” yang sebenarnya tiga langkah.

Alur Diagnosis yang Bisa Diulang #

Diagnosis race yang efektif mengikuti siklus tertutup, karena satu pembersihan sering menyingkap race berikutnya yang tadinya tersembunyi:

LangkahTindakanSarana
1. ReproduksiJalankan beban paralel dari code path yang dicurigaigo run -race, go test -race
2. LokalisasiBaca pasangan akses + titik pembuatan goroutine di laporanstack trace pada WARNING
3. PerbaikanPilih satu: mutex, atomic, atau channel sesuai pola aksessync.Mutex, sync/atomic, channel
4. VerifikasiJalankan ulang sampai nol warning dan exit 0-race di mesin uji dan CI

Langkah 3 di chapter ini memakai sync/atomic — untuk counter murni, atomic.Int64 adalah perbaikan paling langsung. Berkas contoh-fixed.go menuliskan perbaikan itu:

go
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var wg sync.WaitGroup
	var saldo atomic.Int64 // akses diatomisasi lewat tipe atomic

	wg.Add(2)
	go func() {
		defer wg.Done()
		for range 1000 {
			saldo.Add(1)
		}
	}()
	go func() {
		defer wg.Done()
		for range 1000 {
			saldo.Add(1)
		}
	}()

	wg.Wait()
	fmt.Println("saldo akhir:", saldo.Load())
}

Output yang Dihasilkan:

text
saldo akhir: 2000

Jalankan versi perbaikan dengan flag yang sama untuk menutup siklus:

bash
go run -race contoh-fixed.go

Output yang Dihasilkan:

text
saldo akhir: 2000

Tidak ada WARNING, exit code 0, dan hasil selalu 2000 — tiga syarat bersih sekaligus. Bandingkan dengan versi berlomba yang sebelumnya mencetak nilai acak di bawah 2000: variabel atomic membuat setiap Add menjadi operasi tak terpisahkan tanpa mengunci keseluruhan struct.

Setelah program benar dan cepat di satu mesin, tugas terakhir adalah membuatnya tetap bisa didiagnosis setelah berjalan di production — mengambil profil dan metrik dari proses hidup — topik penutup section ini.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh-fixed.go

Unduh berkas
go
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var wg sync.WaitGroup
	var saldo atomic.Int64 // akses diatomisasi lewat tipe atomic

	wg.Add(2)
	go func() {
		defer wg.Done()
		for range 1000 {
			saldo.Add(1)
		}
	}()
	go func() {
		defer wg.Done()
		for range 1000 {
			saldo.Add(1)
		}
	}()

	wg.Wait()
	fmt.Println("saldo akhir:", saldo.Load())
}

contoh-race.go

Unduh berkas
go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	saldo := 0 // variabel bersama: dua goroutine menulis tanpa proteksi

	wg.Add(2)
	go func() {
		defer wg.Done()
		for range 1000 {
			saldo++ // baca, tambah, tulis — tanpa sinkronisasi
		}
	}()
	go func() {
		defer wg.Done()
		for range 1000 {
			saldo++ // akses yang sama persis ke memori yang sama
		}
	}()

	wg.Wait()
	// Nilai saldo sengaja tidak dicetak: nilainya berubah-ubah
	// antar eksekusi (2000 atau kurang) — itulah gejala data race.
	fmt.Println("transaksi selesai")
}