Go Race detector
Data race problems in your concurrent Go programs can be found and diagnosed with the aid of the Go race detector, a built-in feature of the Go compiler. A data race happens when at least one write operation is carried out by one of the instructions that access the same memory address.
How to Use the Go Race Detector
You can use the -race flag when constructing or running a Go source file to activate the race detector:
go run -race your_program.go
go build -race your_program.go
A changed executable file is produced by the compiler when it is activated. All accesses to shared variables and synchronization events that take place during execution, such as calls to sync.Mutex
and sync.WaitGroup
, are recorded by this enhanced version’s instrumentation. Once these events have been examined, the race detector generates a report that assists you in identifying and fixing any possible issues in your code. It is strongly advised to repair any reported races and to use the race detector.
Example of a Data Race Condition with Race Detector
Examine the Go program raceC.go, which has two typical data race conditions:
package main
import (
"fmt"
"os"
"strconv"
"sync"
)
func main() {
arguments := os.Args
if len(arguments) != 2 {
fmt.Println("Give me a natural number!")
os.Exit(1)
}
numGR, err := strconv.Atoi(arguments[1])
if err != nil {
fmt.Println(err)
return
}
var waitGroup sync.WaitGroup
var i int
k := make(map[int]int)
k[1] = 12
for i = 0; i < numGR; i++ {
waitGroup.Add(1)
go func() { // Anonymous function does not capture 'i' as a parameter
defer waitGroup.Done()
k[i] = i // Race condition 1: 'i' is shared. Race condition 2: Concurrent map write.
}()
}
k[2] = 10 // Race condition 2: Concurrent map write by main goroutine.
waitGroup.Wait()
fmt.Printf("k = %#v\n", k)
}
Output
Give me a natural number!
For instance, go run -race raceC when you run raceC.go with the -race flag.The output from the race detector will look something like this when go 10 is used (precise addresses and goroutine IDs may vary):
$ go run -race raceC.go 10
==================
WARNING: DATA RACE
Read at 0x00c42007c008 by goroutine 6:
main.main.func1()
/Users/mtsouk/ch/ch10/code/raceC.go:32 +0x69
Previous write at 0x00c42007c008 by main goroutine:
main.main()
/Users/mtsouk/ch/ch10/code/raceC.go:28 +0x27b
Goroutine 6 (running) created at:
main.main()
/Users/mtsouk/ch/ch10/code/raceC.go:30 +0x24b
==================
==================
WARNING: DATA RACE
Write at 0x00c420074180 by goroutine 7:
runtime.mapassign_fast64()
/usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:598 +0x0
main.main.func1()
/Users/mtsouk/ch/ch10/code/raceC.go:32 +0x90
Previous write at 0x00c420074180 by goroutine 6:
runtime.mapassign_fast64()
/usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:598 +0x0
main.main.func1()
/Users/mtsouk/ch/ch10/code/raceC.go:32 +0x90
Goroutine 7 (running) created at:
main.main()
/Users/mtsouk/ch/ch10/code/raceC.go:30 +0x24b
Goroutine 6 (finished) created at:
main.main()
/Users/mtsouk/ch/ch10/code/raceC.go:30 +0x24b
==================
k = map[int]int{3:4, 5:5, 9:9, 10:10, 1:1, 2:10, 4:4, 6:6, 7:7, 8:8}
Found 2 data race(s)
exit status 66
Explanation of the Race Detector Output:
The results reveal two data races:
Race on loop variable i: The loop variable i is not explicitly captured as an argument by the anonymous function that is launched as a goroutine, which leads to the initial data race. Rather, the i declared in the scope of the main function is referred to. Other goroutines may read erratic, distinct values of i as it changes in the for loop (a write operation by the main goroutine), producing inconsistent behaviour. The goroutine’s “Read” and the main goroutine’s “Previous write” to the same memory address linked to i are highlighted by the detector.
Concurrent writes to the k map: Multiple goroutines (designated as main.main.func1()) attempt to write to the shared map k at the same time in the second data race. The main goroutine additionally concurrently writes to k with k = 10. It is unsafe and a data race to make direct concurrent writes to a Go map without adequate synchronisation.
Fixing the Loop Variable Race Condition
In order to resolve the loop variable i’s initial race condition, it must be supplied as a parameter to the anonymous function, thereby generating a duplicate of i for every goroutine. Consequently, the noRaceC.go
program is created:
package main
import (
"fmt"
"os"
"strconv"
"sync"
)
func main() {
arguments := os.Args
if len(arguments) != 2 {
fmt.Println("Give me a natural number!")
os.Exit(1)
}
numGR, err := strconv.Atoi(arguments[1])
if err != nil {
fmt.Println(err)
return
}
var waitGroup sync.WaitGroup
// var i int // 'i' is no longer declared here to be shared implicitly.
k := make(map[int]int)
k[1] = 12
for i := 0; i < numGR; i++ { // 'i' is declared with short declaration inside the loop's scope
waitGroup.Add(1)
go func(i int) { // 'i' is passed as a parameter, capturing its value for each goroutine
defer waitGroup.Done()
k[i] = i // Still a concurrent map write, but 'i' itself is no longer racing
}(i) // Pass the current value of 'i' to the goroutine
}
k[2] = 10
waitGroup.Wait()
fmt.Printf("k = %#v\n", k)
}
Output
Give me a natural number!
No data races pertaining to the i variable are now displayed when noRaceC.go is run with the -race flag (go run -race noRaceC.go 10).
$ go run -race noRaceC.go 10
k = map[int]int{5:5, 7:7, 9:9, 1:1, 0:0, 4:4, 6:6, 8:8, 2:10, 3:3}
According to the race detector report, this particular problem has been fixed. It’s crucial to remember, though, that the noRaceC.go
example still includes a data race because of concurrent writes to the shared map k without a locking mechanism, even after rectifying the loop variable capture. Go’s runtime explicitly recognises and panics on concurrent unsynchronized map updates, as evidenced by fatal error: concurrent map writes, which can occur if this particular map write race is ignored.
The runtime’s internal checks may discover this kind of problem before the race detector does, even if it isn’t reported as a “data race”. To completely fix such problems, access to the shared map would need to be protected using synchronisation primitives like sync.Mutex
or channels.
You can also read Mutexes In GoLang Used To Prevents Threads Or goroutines