A data race in Go (or any other programming language) occurs when two or more goroutines access the same shared data concurrently and at least one of those accesses is a write operation. This can lead to unpredictable behavior and can cause bugs in your code.
In Go, data races can occur when multiple goroutines access the same variables or memory locations without proper synchronization. For example, consider the following code:
package main
var counter int
func incrementCounter() {
counter++
}
func main() {
go incrementCounter()
go incrementCounter()
}
In this code, two goroutines are incrementing the same counter
variable concurrently. This can lead to a data race because both goroutines are writing to the same memory location without any synchronization mechanism. How to find out it race condition occurred ? Try run with flag --race
like example below
❯ go run --race main.go
==================
WARNING: DATA RACE
Read at 0x000000548700 by goroutine 7:
main.incrementCounter()
/home/upix/code/oss/golang-playground/main.go:6 +0x29
Previous write at 0x000000548700 by goroutine 6:
main.incrementCounter()
/home/upix/code/oss/golang-playground/main.go:6 +0x44
Goroutine 7 (running) created at:
main.main()
/home/upix/code/oss/golang-playground/main.go:11 +0x35
Goroutine 6 (finished) created at:
main.main()
/home/upix/code/oss/golang-playground/main.go:10 +0x29
==================
Found 1 data race(s)
exit status 66
To avoid data races in Go, you need to use synchronization primitives such as channels, mutexes, or atomic operations. For example, you can use a mutex to ensure that only one goroutine at a time can access the shared data:
package main
var counter int
var mu sync.Mutex
func incrementCounter() {
mu.Lock()
defer mu.Unlock()
counter++
}
func main() {
go incrementCounter()
go incrementCounter()
}
With this implementation, the mutex ensures that only one goroutine can access the counter
variable at a time, avoiding any data races.
The other alternative is use the atomic
package in Go to perform atomic operations on shared variables, ensuring that they are executed as a single indivisible operation. For example:
package main
import "sync/atomic"
var counter int32
func incrementCounter() {
atomic.AddInt32(&counter, 1)
}
func main() {
go incrementCounter()
go incrementCounter()
}