Lỗi Xảy Ra
Bạn chạy chương trình Go với race detector được bật và nhận được thông báo kiểu như này:
==================
WARNING: DATA RACE
Write at 0x00c0000b4010 by goroutine 7:
main.incrementCounter()
/home/user/app/main.go:15 +0x30
Previous read at 0x00c0000b4010 by goroutine 6:
main.readCounter()
/home/user/app/main.go:22 +0x2e
Goroutine 7 (running) created at:
main.main()
/home/user/app/main.go:35 +0x6e
==================
Found 1 data race(s)
exit status 66
Hoặc race detector kích hoạt giữa chừng khi chạy go test -race ./... và pipeline CI của bạn báo đỏ. Dù là trường hợp nào, đây là vấn đề nghiêm trọng — dữ liệu bị hỏng âm thầm trên môi trường production rất khó debug và càng khó tái hiện hơn.
Nguyên Nhân
Data race xảy ra khi hai goroutine truy cập cùng một vùng nhớ đồng thời và ít nhất một trong số đó là thao tác ghi — mà không có bất kỳ cơ chế đồng bộ nào giữa chúng. Race detector của Go runtime (xây dựng trên ThreadSanitizer) phát hiện điều này bằng cách theo dõi các thao tác truy cập bộ nhớ lúc runtime.
Những nguyên nhân thường gặp:
- Một bộ đếm toàn cục được tăng bởi nhiều goroutine mà không có mutex
- Một
mapbị ghi và đọc đồng thời — map hoàn toàn không an toàn với goroutine - Một slice bị append từ nhiều goroutine cùng lúc
- Một trường trong struct bị cập nhật ở một goroutine trong khi goroutine khác đang đọc nó
Bước 1 — Bật Race Detector
Chưa chạy với -race? Hãy bật lên. Output sẽ cho bạn biết chính xác file nào, dòng nào, và goroutine nào gây ra race:
# Chạy binary với race detection
go run -race main.go
# Chạy test với race detection
go test -race ./...
# Build binary có tích hợp race instrumentation
go build -race -o myapp .
Đọc kỹ các stack trace. Chúng cho biết goroutine nào đã ghi, goroutine nào đã đọc, và mỗi goroutine được tạo ra ở đâu. Bắt đầu từ đó.
Bước 2 — Sửa Race Condition
Chọn cách tiếp cận phù hợp với tình huống của bạn.
Cách A: sync.Mutex (cách sửa phổ biến nhất)
Bảo vệ các biến dùng chung bằng mutex. Một goroutine giữ lock; tất cả các goroutine còn lại phải chờ.
Trước (bị lỗi):
var counter int
func increment() {
counter++ // DATA RACE: nhiều goroutine cùng truy cập vào đây
}
func main() {
for i := 0; i < 1000; i++ {
go increment()
}
}
Sau (đã sửa với Mutex):
import "sync"
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println(counter) // luôn là 1000
}
Cách B: sync.RWMutex (workload đọc nhiều)
Đọc nhiều, ghi ít? RWMutex cho phép nhiều goroutine đọc đồng thời — chỉ thao tác ghi mới chặn tất cả:
var (
data map[string]string
mu sync.RWMutex
)
func read(key string) string {
mu.RLock()
defer mu.RUnlock()
return data[key]
}
func write(key, value string) {
mu.Lock()
defer mu.Unlock()
data[key] = value
}
Cách C: sync/atomic (bộ đếm và cờ đơn giản)
Các bộ đếm kiểu integer đơn giản và cờ boolean không cần mutex đầy đủ. sync/atomic nhanh hơn và hoàn toàn tránh được lock contention:
import "sync/atomic"
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func getCount() int64 {
return atomic.LoadInt64(&counter)
}
Cách D: Channel (giao tiếp thay vì chia sẻ)
Channel truyền quyền sở hữu dữ liệu giữa các goroutine mà không cần chia sẻ bộ nhớ. Không có state dùng chung, không có race:
func worker(jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
go worker(jobs, results)
jobs <- 5
close(jobs)
fmt.Println(<-results) // 10
}
Cách E: sync.Map (truy cập map đồng thời)
Bị race trên map? Thay thế trực tiếp bằng sync.Map. Không cần thêm locking:
var m sync.Map
// Ghi
m.Store("key", "value")
// Đọc
val, ok := m.Load("key")
if ok {
fmt.Println(val.(string))
}
Bước 3 — Kiểm Tra Lại
Chạy lại với race detector. Không còn race nào nghĩa là bạn đã xong:
go test -race ./...
# Kết quả mong đợi (không còn race):
ok github.com/yourorg/app 0.512s
go run -race main.go
# Chương trình chạy mà không xuất hiện WARNING: DATA RACE
Muốn chắc chắn hơn? Tăng mức đồng thời và chạy nhiều lần:
# Chạy test với mức song song cao
go test -race -count=10 -parallel=8 ./...
Sạch trên 10 lần chạy song song? Bạn đã sửa xong.
Kinh Nghiệm Thực Tế
- Luôn chạy
-racetrong CI. Detector thêm 2–20x overhead CPU và 5–10x overhead bộ nhớ — quá tốn kém cho production, nhưng hoàn toàn ổn cho các lần chạy test. - Không bao giờ copy
sync.Mutex. Truyền mutex theo giá trị sẽ âm thầm làm hỏng nó. Luôn dùng con trỏ, hoặc nhúng mutex vào struct được truyền theo con trỏ. - Channel dùng để truyền dữ liệu; mutex dùng để bảo vệ state dùng chung. Cứ mỗi thao tác lại dùng mutex là dấu hiệu thiết kế có vấn đề — hãy xem lại cấu trúc goroutine của bạn.
- Closure trong goroutine là bẫy kinh điển. Biến vòng lặp bị closure bắt là nguồn gốc race sách giáo khoa. Hãy truyền biến đó vào như một đối số thay thế:
// LỖI: tất cả goroutine cùng bắt một biến `i`
for i := 0; i < 5; i++ {
go func() { fmt.Println(i) }()
}
// ĐÃ SỬA: truyền `i` vào như đối số
for i := 0; i < 5; i++ {
go func(i int) { fmt.Println(i) }(i)
}

