Data Races
When writing concurrent applications, it is common to run into what is called a race condition.15 A race condition occurs when two different goroutines try to access the same shared resource.
Consider Listing 13.25. There are two different goroutines. One goroutine inserts values into a map, and the other goroutine ranges over the map and prints the values.
Listing 13.25 Two Goroutines Accessing a Shared Map
// launch a goroutine to // write data in the map go func() { for i := 0; i < 10; i++ { // loop putting data in the map data[i] = true } // cancel the context cancel() }() // launch a goroutine to // read data from the map go func() { // loop through the map // and print the keys/values for k, v := range data { fmt.Printf("%d: %v\n", k, v) } }()
In Listing 13.26, we use those two goroutines to write a test to assert the map is written to and read from correctly.
Listing 13.26 A Passing Testing Without the Race Detector
func Test_Mutex(t *testing.T) { t.Parallel() // create a new cancellable context // to stop the test when the goroutines // are finished ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, 20*time.Millisecond) defer cancel() // create a map to be used // as a shared resource data := map[int]bool{} // launch a goroutine to // write data in the map go func() { for i := 0; i < 10; i++ { // loop putting data in the map data[i] = true } // cancel the context cancel() }() // launch a goroutine to // read data from the map go func() { // loop through the map // and print the keys/values for k, v := range data { fmt.Printf("%d: %v\n", k, v) } }() // wait for the context to be canceled <-ctx.Done() if len(data) != 10 { t.Fatalf("expected 10 items in the map, got %d", len(data)) } }
$ go test -v === RUN Test_Mutex === PAUSE Test_Mutex === CONT Test_Mutex --- PASS: Test_Mutex (0.00s) PASS ok demo 0.471s
Go Version: go1.19
A quick glance at the test output, Listing 13.26, would seem to imply that the tests have passed successfully, but this is not the case.
The Race Detector
A few of the Go commands, such as test and build, have the -race flag exposed. When used, the -race flag tells the Go compiler to create a special version of the binary or test binary that will detect and report race conditions.
If we run the test again, this time with the -race flag, we get a very different result, as shown in Listing 13.27.
Listing 13.27 Tests Failing with the Race Detector
$ go test -v -race === RUN Test_Mutex === PAUSE Test_Mutex === CONT Test_Mutex --- PASS: Test_Mutex (0.00s) ================== WARNING: DATA RACE Read at 0x00c00011c3f0 by goroutine 9: runtime.mapdelete() /usr/local/go/src/runtime/map.go:695 +0x46c demo.Test_Mutex.func2() ./demo_test.go:46 +0x50 Previous write at 0x00c00011c3f0 by goroutine 8: runtime.mapaccess2_fast64() /usr/local/go/src/runtime/map_fast64.go:53 +0x1cc demo.Test_Mutex.func1() ./demo_test.go:32 +0x50 Goroutine 9 (running) created at: demo.Test_Mutex() ./demo_test.go:43 +0x188 testing.tRunner() /usr/local/go/src/testing/testing.go:1439 +0x18c testing.(*T).Run.func1() /usr/local/go/src/testing/testing.go:1486 +0x44 Goroutine 8 (finished) created at: demo.Test_Mutex() ./demo_test.go:28 +0x124 testing.tRunner() /usr/local/go/src/testing/testing.go:1439 +0x18c testing.(*T).Run.func1() /usr/local/go/src/testing/testing.go:1486 +0x44 ================== FAIL exit status 1 FAIL demo 0.962s
Go Version: go1.19
As you can see from the output, the Go race detector found a race condition in our code.
If we examine the top two entries in a race condition warning, Listing 13.28, it tells us where the two conflicting lines of code are.
Listing 13.28 Reading the Race Detector Output
Read at 0x00c00018204b by goroutine 9: demo.Test_Mutex.func2() problem/demo_test.go:46 +0xa5 Previous write at 0x00c00018204b by goroutine 8: demo.Test_Mutex.func1() problem/demo_test.go:32 +0x5c
A read of the shared resource was happening at demo_test.go:46, and a write was happening at demo_test.go:32. We need to synchronize or lock these two goroutines so that they don’t both try to access the shared resource at the same time.
Most, but Not All
The Go race detector makes a simple guarantee with you (the end user).
A race condition will panic and crash your application. If the race detector finds a race condition, you must fix it.
Wrapping Up the Race Detector
The race detector is an invaluable tool when developing Go applications. When running tests with the -race flag, you will notice a slowdown in test performance. The race detector has to do a lot of work to track those conditions.
Once identified, the sync package, Listing 13.29, provides a number of ways that you can fix issues.
Listing 13.29 The sync Package
$ go doc -short sync type Cond struct{ ... } func NewCond(l Locker) *Cond type Locker interface{ ... } type Map struct{ ... } type Mutex struct{ ... } type Once struct{ ... } type Pool struct{ ... } type RWMutex struct{ ... } type WaitGroup struct{ ... }
Go Version: go1.19