Golang Project Structure

Tips and tricks for writing and structuring Go code

Are Golang Maps Thread-Safe?

Language

  • unknown

by

Concurrency is such an integral part of Golang, because modern computers tend to have multiple processors and threads that code can run on. It’s therefore important that the data structures we use can be used across multiple threads — or, in our language, goroutines.

A map in Golang is equivalent to a dictionary in Python or an object in JavaScript. It simply allows us to store data as key-value pairs.

Initalizing Maps

Before it can be used, each map has to be initialized with the make function. The key-type goes inside the square brackets and the value-type follows them. So in the example below, we’re storing an integer for each string. Let’s say that we’re counting the number of cats and dogs that are housed in kennels that we run.

package main

import (
	"fmt"
)

func main() {
	m := make(map[string]int)

	m["dog"] = 100
	m["cat"] = 101

	fmt.Println(m) // map[cat:101 dog:100]
}

Reading Is Easy

We can read the map concurrently. For example, we could start five goroutines and get them each to print out the stringified map, and that wouldn’t pose a problem.

package main

import (
	"fmt"
	"sync"
)

func main() {
	m := make(map[string]int)

	m["dog"] = 100
	m["cat"] = 101

	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1)

		go func(i int) {
			defer wg.Done()
		
			fmt.Println(i, m)
		}(i)
	}

	wg.Wait()
}

We use the sync.WaitGroup data structure, so that the main function doesn’t end until all the goroutines that are started in the for-loop have completed. Each goroutine calls a method on the WaitGroup to show that it has finished: it does this using the defer keyword, so that the other code in the goroutine is run first.

Writing With a Mutex

However, we cannot safely write to maps concurrently. So in the example below where we start up goroutines to increment the map values, it’s necessary to use a mutex. Note that we use a defer statement to trigger an automatic unlock of the mutex when the function ends. Also note that we still don’t need to lock the mutex when we’re reading — unless another goroutine may be writing at the same time, which may often be the case in real-world code.

package main

import (
	"fmt"
	"sync"
)

func main() {
	m := make(map[string]int)
	var mutex sync.Mutex
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()

			defer mutex.Unlock()
			mutex.Lock()

			m["dog"]++
		}()
	}
	
	for i := 0; i < 101; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()

			defer mutex.Unlock()
			mutex.Lock()

			m["cat"]++
		}()
	}

	wg.Wait()

	fmt.Println(m)
}

Writing With a Synchronized Map

Because concurrent goroutines are used so commonly in Go, the standard library has a specialized data structure that acts like a normal map, but is perfectly safe to be read and written to across multiple goroutines without the use of an external mutex. The sync.Map type stores keys and values of any type (using the empty interface). In the example below, we also use the sync/atomic package, so that we can increment values in different goroutines without running into a data race.

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var m sync.Map
	var wg sync.WaitGroup

	m.Store("dog", new(int64))
	m.Store("cat", new(int64))

	for i := 0; i < 100; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()

			if ptrInterface, ok := m.Load("dog"); ok {
				if ptr, ok := ptrInterface.(*int64); ok {
					atomic.AddInt64(ptr, 1)
				}
			}
		}()
	}

	for i := 0; i < 101; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()

			if ptrInterface, ok := m.Load("cat"); ok {
				if ptr, ok := ptrInterface.(*int64); ok {
					atomic.AddInt64(ptr, 1)
				}
			}
		}()
	}

	wg.Wait()

	m.Range(func(key, value interface{}) bool {
		if ptr, ok := value.(*int64); ok {
			fmt.Println(key, *ptr)
		}

		return true
	})
}

Conclusion

Using a map is an easy way to store values associated with keys. If you only have a main goroutine, you don’t need to worry about concurrency issues. Likewise, if you only ever write to each map from a single goroutine, you’ll be fine. However, if you want to access the same map across multiple goroutines, then you will need to take some action in order to make sure your data does not get corrupted: the two ways we have seen to do that in this blog post involve using either a normal map with a mutex or a specialized synchronized map from the standard library.

Leave a Reply

Your email address will not be published.