Go race detector

Recently, we developed a web application which involved the use of multiple goroutines. As a result data races were just around the corner. Fortunately, Go comes with go race detector — an incredible tool which makes them easier to detect.

Let’s look at a simplified example:

var servedDevices []Device

devicesToProcess := make(chan []Device, 1)
go func(devicesToProcess chan[]Device) {
    select {
    case devices := <- devicesToProcess:
        for _, device := range devices {
            go func(device Device) {
                // Operations on Device
                servedDevices = append(servedDevices, device)
            }(device)
        }
    }
}(devicesToProcess)

A channel for slices of Devices is passed as an argument to a goroutine. It reads from the channel and every time a slice comes in, it in turn kicks of a separate goroutine for each Device to perform some work with that device. When finished, it appends the Device to a slice defined in the main goroutine.

The program seems to work as expected but using race flag many data races occurred on writing to servedDevices. The tool provides a essential information about the events:

go run -race main.go
WARNING: DATA RACE
Read at 0x000000568650 by goroutine 8:
  main.main.func1.1()
      main.go:23 +0x41

Previous write at 0x000000568650 by goroutine 7:
  main.main.func1.1()
      main.go:23 +0xdf

Goroutine 8 (running) created at:
  main.main.func1()
      main.go:26 +0x101

Goroutine 7 (finished) created at:
  main.main.func1()
     main.go:26 +0x101
==================
==================
WARNING: DATA RACE
Read at 0x00c42007a190 by goroutine 8:
  runtime.growslice()
      slice.go:71 +0x0
  main.main.func1.1()
      main.go:23 +0x197

Previous write at 0x00c42007a190 by goroutine 7:
  main.main.func1.1()
      main.go:23 +0xa1

Goroutine 8 (running) created at:
  main.main.func1()
      main.go:26 +0x101

Goroutine 7 (finished) created at:
  main.main.func1()
      main.go:26 +0x101
==================
Found 2 data race(s)
exit status 66

Some goroutine conflicts are detected on appending a new Device to a slice. append is an atomic operation but it is not thread safe.: when the slice is modified by multiple goroutines, the result is not deterministic. Effect of one goroutine might be reflected, or the other or both. In order to make the list thread safe we can use both a mutex or another channel which will be read in the main goroutine and append to the list in one place. Let’s try the mutex:

type ServedDevices struct {
	devices []Device
	mutex   sync.Mutex
}

func (s *ServedDevices) Add (device Device){
	s.mutex.Lock()
	defer s.mutex.Unlock()
	s.devices = append(s.devices, device)
}

ServedDevices is thread save because it wraps thread unsafe operation (slice append) by a mutex which serializes access to the slice. ServedDevices could be used as type for servedDevices variable and Add function in instead of raw append.

As written in the documentation, data races are detected only in running code. For that reason, testing must be as realistic as possible. Moreover, CPU cost and Memory usage could be up to ten times greater than normal use.

Anyway, its flexibility and completeness makes it one of our essential tool for testing applications.

We're looking for developers to help us save energy

If you're interested in what we do and you would like to help us save energy, drop us a line at jobs@enectiva.cz.

comments powered by Disqus