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:
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:
transaksi selesaiEksekusi biasa keluar 0 dan tampak normal — bahaya inilah yang membuat race lolos dari pengujian manual. Sekarang jalankan berkas yang sama dengan flag -race:
go run -race contoh-race.goOutput yang Dihasilkan:
==================
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 66Membaca 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:
| Langkah | Tindakan | Sarana |
|---|---|---|
| 1. Reproduksi | Jalankan beban paralel dari code path yang dicurigai | go run -race, go test -race |
| 2. Lokalisasi | Baca pasangan akses + titik pembuatan goroutine di laporan | stack trace pada WARNING |
| 3. Perbaikan | Pilih satu: mutex, atomic, atau channel sesuai pola akses | sync.Mutex, sync/atomic, channel |
| 4. Verifikasi | Jalankan 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:
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:
saldo akhir: 2000Jalankan versi perbaikan dengan flag yang sama untuk menutup siklus:
go run -race contoh-fixed.goOutput yang Dihasilkan:
saldo akhir: 2000Tidak 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 #
- Introducing the Go Race Detector — artikel resmi tim Go tentang desain dan biaya race detector.
- Data Race Detector
— panduan resmi memakai
-racepada run, test, dan build. - Package sync/atomic — dokumentasi tipe atomic untuk perbaikan race.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh-fixed.go
Unduh berkaspackage 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 berkaspackage 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")
}