Race condition là lỗi logic: kết quả phụ thuộc thứ tự thực thi. Nó xảy ra kể cả khi khóa đúng — ví dụ if !exists(k) { insert(k) } với mỗi lời gọi tự khóa riêng vẫn cho hai lần insert.
Data race hẹp hơn: hai goroutine truy cập cùng một ô nhớ, ít nhất một là ghi, và giữa chúng không có quan hệ happens-before.
Go memory model định nghĩa happens-before qua các mốc đồng bộ: gửi trên channel xảy ra trước khi lần nhận tương ứng hoàn tất; close xảy ra trước lần nhận thấy channel đã đóng; Unlock thứ n xảy ra trước Lock thứ n+1; once.Do hoàn tất trước mọi lời gọi Do khác trả về; các phép sync/atomic hoạt động như atomic tuần tự nhất quán.
var data string
done := make(chan struct{})
go func() {
data = "ready" // (1)
close(done) // (2) happens-before the receive below
}()
<-done
fmt.Println(data) // safe: (1) is visible hereKhông có "benign race". Memory model của Go nhẹ tay hơn C/C++ ở chỗ chương trình có race không bị coi là hoàn toàn vô định nghĩa: mỗi lần đọc một ô nhớ cỡ một word sẽ thấy một giá trị nào đó đã thực sự được ghi vào đó. Nhưng bảo đảm ấy chỉ áp cho giá trị một word. Giá trị nhiều word — interface, slice, string, map header — có thể bị đọc ở trạng thái nửa cũ nửa mới, dẫn tới con trỏ trỏ sai kiểu và crash thật (fatal error: concurrent map read and map write).
Phát hiện: go build/test/run -race. Nó là detector runtime, chỉ bắt được race thực sự xảy ra trong lần chạy đó, đổi lại chậm khoảng 2–20 lần và tốn thêm bộ nhớ. Nên chạy -race cho test tích hợp trong CI, và cân nhắc bật trên một instance canary ở staging.