Home > Articles

This chapter is from the book

This chapter is from the book

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

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.