Skip to content Skip to sidebar Skip to footer

Synchronizing Goroutines in Go: Using sync.Mutex and sync.Once

When you write concurrent programs in Go, multiple goroutines may try to access and modify the same data at the same time. Without proper synchronization, this leads to race conditions, bugs, or crashes. Go provides tools like sync.Mutex, sync.RWMutex, and sync.Once to safely share data across goroutines.

In this article, you’ll learn:

  • What race conditions are and how to avoid them
  • How to use sync.Mutex to protect data
  • Using sync.RWMutex for read-write access
  • How sync.Once ensures code runs only once
  • Real-world examples and best practices

What Is a Race Condition?

A race condition happens when two or more goroutines access the same variable at the same time, and at least one of them is modifying it. This can cause unexpected behavior or corrupted data.

You can detect race conditions using:

go run -race main.go

Using sync.Mutex

sync.Mutex is a mutual exclusion lock. Only one goroutine can hold the lock at a time. Use Lock() before accessing shared data, and Unlock() after.

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

Using sync.RWMutex

sync.RWMutex allows multiple readers or one writer. It's useful when reads are frequent but writes are rare.

type SafeMap struct {
    mu  sync.RWMutex
    m   map[string]string
}

func (s *SafeMap) Get(key string) string {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.m[key]
}

func (s *SafeMap) Set(key, value string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = value
}

Using sync.Once

sync.Once guarantees that a piece of code is only executed once, even if called from multiple goroutines. This is commonly used to initialize shared resources.

var once sync.Once

func initialize() {
    fmt.Println("Initialization done")
}

func main() {
    for i := 0; i < 5; i++ {
        go func() {
            once.Do(initialize)
        }()
    }
    time.Sleep(time.Second)
}

Real-World Example: Safe Counter

type SafeCounter struct {
    mu sync.Mutex
    val int
}

func (sc *SafeCounter) Add() {
    sc.mu.Lock()
    sc.val++
    sc.mu.Unlock()
}

func main() {
    var sc SafeCounter
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            sc.Add()
            wg.Done()
        }()
    }

    wg.Wait()
    fmt.Println("Final count:", sc.val)
}

Best Practices

  • Always use defer Unlock() right after Lock()
  • Keep the locked section as short as possible
  • Use RWMutex when many goroutines only need to read
  • Use sync.Once to initialize global/shared data
  • Test with go run -race to catch race conditions

Conclusion

Synchronization is key to building correct concurrent programs. By using sync.Mutex, sync.RWMutex, and sync.Once, you can ensure that your goroutines work together safely without corrupting shared data.

Happy coding!

Post a Comment for "Synchronizing Goroutines in Go: Using sync.Mutex and sync.Once"