Mutex

In a concurrent application, you may encounter the problem of multiple processess try to modify a variable at the exact same time. This can lead to incorrect behavior, and the phenomenon is known as a race condition.

You can use sync.Mutex to lock a variable that multiple goroutines want to modify concurrently, preventing other goroutine from accessing it for a short period to ensure the modification happens safely. After the necessary changes, you can release the lock, allowing other concurrently running processes to access it again.

While this approach is widely used in other languages, Go recommends using channels for synchronization. However, in some cases, using a Mutex can be simpler. A prime example is implementing a Counter, which you can see in practise below:

package main
 
import (
	"fmt"
	"sync"
	"time"
)
 
type Counter struct {
	sync.Mutex
	value int
}
 
func main() {
	counter := Counter{}
	for i := 0; i < 10; i++ {
		go func(i int) {
			counter.Lock()
			counter.value++
			defer counter.Unlock()
		}(i)
	}
	time.Sleep(time.Second)
	counter.Lock()
	defer counter.Unlock()
 
	fmt.Println("counter", counter.value)
}

You can run the above example here (opens in a new tab).

In the example above, the Counter type embeds the sync.Mutex type, which allows the counter variable to have Lock and Unlock methods. These methods help block modifications by other goroutines while one goroutine is making changes.

Inside a loop, multiple goroutines are executed, each calling anonymous functions that increment the counter's value by one. Before incrementing the value, the Lock method locks the counter object from other goroutine, and after the value is incremented, the Unlock method releases the lock. Finally, the program prints the value on the main goroutine.

Read - Write mutex

RWMutex allows multiple concurrent readers, but only one writer at a time.

package main
 
import (
	"fmt"
	"sync"
	"time"
)
 
type SafeData struct {
	value int
	mu    sync.RWMutex
}
 
func (s *SafeData) Read() int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.value
}
 
func (s *SafeData) Write(val int) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.value = val
}
 
func main() {
	data := SafeData{}
	var wg sync.WaitGroup
 
	// Writer goroutine
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 1; i <= 5; i++ {
			data.Write(i)
			fmt.Println("Written:", i)
			time.Sleep(100 * time.Millisecond)
		}
	}()
 
	// Reader goroutines
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 5; j++ {
				val := data.Read()
				fmt.Printf("Reader %d read: %d\n", id, val)
				time.Sleep(50 * time.Millisecond)
			}
		}(i)
	}
 
	wg.Wait()
}