How to avoid data race in concurrent Golang programs

What is a data race?

In programs involving Goroutines, a data race occurs when two Goroutines access the same variable concurrently and at least one of the accesses is a write.

Avoid data race with Mutex

One of the typical examples of a data race is a shared variablelike a hash map being updated by multiple Goroutines, thus corrupting the stored data.

Count by letters in a word with data race

Let’s look at a program that counts the occurrences of each letter in a word through Goroutines.

This program involves a hash map that maintains the count by each letter. Due to the concurrent execution of the Goroutines, if we do not handle data race, the result will be erratic.

package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
fmt.Println("Hello, playground")
myMap := make(map[string]int)
for _, c := range "abcaaaaaaaabb" {
wg.Add(1)
go func(letter string, hm map[string]int, wg *sync.WaitGroup) {
defer wg.Done()
r := rand.Intn(10)
time.Sleep(time.Duration(r) * time.Second)
hm[letter] = hm[letter] + 1
}(string(c), myMap, &wg)
}
wg.Wait()
for k, v := range myMap {
fmt.Println(k, v)
}
}

In the code above, there is a potential data race in line 23:

hm[letter] = hm[letter] + 1

This same hashmap is passed (by reference) across multiple Goroutines. This results in an error:

fatal error: concurrent map read and map write

In order to simulate data race, we have introduced a random time delay (in seconds) in line 22:

time.Sleep(time.Duration(r) * time.Second)

Count by letters in a word without Data Race

We can correct this behavior by forcefully serializing the updates to the shared variable. Let’s look at the below code to see how this is done.

package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
fmt.Println("Hello, playground")
myMap := make(map[string]int)
var mutex = &sync.Mutex{}
for _, c := range "abcaaaaaaaabb" {
wg.Add(1)
go func(letter string, hm map[string]int, wg *sync.WaitGroup) {
defer wg.Done()
r := rand.Intn(10)
time.Sleep(time.Duration(r) * time.Second)
mutex.Lock()
hm[letter] = hm[letter] + 1
mutex.Unlock()
}(string(c), myMap, &wg)
}
wg.Wait()
for k, v := range myMap {
fmt.Println(k, v)
}
}

Note that in the above code, we have fixed the data race by acquiring a MutexMutual Exclusion lock.

mutex.Lock()
hm[letter] = hm[letter] + 1
mutex.Unlock()

This is one of the common ways of protecting & serializing updates of a shared variable.

Global counters

Sometimes, we encounter scenarios where we want to safely update a global counter across multiple Goroutines.

Use sync/atomic package and implement such atomic counters.

Below, we see an example of how to atomically update a global counter across multiple Goroutine executions.

package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter uint64
var wg sync.WaitGroup
for i := 0; i < 50000; i++ {
wg.Add(1)
go func() {
atomic.AddUint64(&counter, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println("counter:", counter)
}

Free Resources

Copyright ©2025 Educative, Inc. All rights reserved