Go “Sync” Package: Where the magic happens.

Chidozie C. Okafor
5 min readDec 21, 2022

--

The sync package in Go provides a set of types and functions that can be used to synchronize access to shared resources. Two of the most commonly used types in the sync package are WaitGroup and Mutex.

WaitGroup is a type that allows you to wait for a group of goroutines to finish executing. It has three methods: Add, Done, and Wait.

Add is used to specify the number of goroutines that the WaitGroup is waiting for. Done is called when a goroutine has finished executing, and Wait blocks the calling goroutine until all goroutines have finished executing.

Here’s an example of how you can use a WaitGroup to wait for two goroutines to finish executing:

package main

import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup

// Add two goroutines to the WaitGroup
wg.Add(2)

// Launch the goroutines
go func() {
defer wg.Done()
fmt.Println("Goroutine 1")
}()
go func() {
defer wg.Done()
fmt.Println("Goroutine 2")
}()

// Wait for the goroutines to finish
wg.Wait()
fmt.Println("All goroutines finished")
}

In this example, the main goroutine launches two goroutines and adds them to the WaitGroup using the Add method. The goroutines call the Done method when they have finished executing, and the main goroutine calls the Wait method to block until all goroutines have finished.

Mutex is a type that provides mutual exclusion, which means that it allows only one goroutine to access a shared resource at a time. It has two methods: Lock and Unlock.

Lock is used to acquire the mutex and block other goroutines from accessing the shared resource, and Unlock is used to release the mutex and allow other goroutines to access the resource.

Here’s an example of how you can use a Mutex to synchronize access to a shared variable:

package main

import (
"fmt"
"sync"
)

var count int
var mutex sync.Mutex

func main() {
var wg sync.WaitGroup

// Add three goroutines to the WaitGroup
wg.Add(3)

// Launch the goroutines
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
mutex.Lock()
count++
mutex.Unlock()
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
mutex.Lock()
count++
mutex.Unlock()
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10;
mutex.Lock()
count++
mutex.Unlock()
}
}()

// Wait for the goroutines to finish
wg.Wait()
fmt.Println("Count:", count)
}

In this example, three goroutines are launched and each increment the count variable 10 times. To ensure that only one goroutine can access the count variable at a time, they acquire the mutex using the Lock method before accessing the variable and release it using the Unlock method after they are done.

Using WaitGroup and Mutex can be an effective way to synchronize access to shared resources in Go and ensure that your program behaves correctly. However, it's important to be mindful of the performance overhead of using these types and to use them only when necessary.

In addition to WaitGroup and Mutex, the sync package in Go provides several other types and functions for synchronizing access to shared resources.

One of these types is RWMutex, which is a mutex that allows multiple readers to access a shared resource simultaneously, but only one writer at a time. This can be useful when you have a resource that is read frequently but written to less often. RWMutex has the same Lock and Unlock methods as Mutex, but also has RLock and RUnlock methods for read locks.

Another type in the sync package is Once, which allows you to ensure that a piece of code is only executed once. It has a single method called Do, which takes a function as an argument and executes it if it has not been called before. This can be useful for initializing resources that should only be initialized once, such as a database connection or a log file.

Here’s an example of how you can use Once to initialize a global variable:

package main

import (
"fmt"
"sync"
)

var count int
var once sync.Once

func main() {
once.Do(func() {
count = 10
fmt.Println("Count initialized")
})

fmt.Println("Count:", count)
}

In this example, the once.Do function is called to initialize the count variable. The function passed to Do will only be executed the first time it is called, ensuring that the count variable is only initialized once.

The sync package also provides a Pool type, which allows you to recycle and reuse temporary objects to avoid the overhead of constantly creating and destroying them. This can be especially useful when you are creating and destroying large numbers of objects, as it can help reduce memory allocation and garbage collection overhead.

The sync package also provides several utility functions, such as NewCond and NewBarrier, which allow you to create Cond and Barrier types.

Cond is a type that allows you to signal and wait on a condition. It is often used in conjunction with a Mutex to synchronize access to a shared resource. Barrier is a type that allows a group of goroutines to wait for each other to reach a certain point before continuing.

Here’s an example of how you can use a Cond to synchronize access to a shared resource:

package main

import (
"fmt"
"sync"
)

var count int
var mutex sync.Mutex
var cond *sync.Cond

func main() {
cond = sync.NewCond(&mutex)

var wg sync.WaitGroup
wg.Add(2)

go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
mutex.Lock()
for count == 0 {
cond.Wait()
}
count--
mutex.Unlock()
fmt.Println("Goroutine 1: count =", count)
}
}()
go func() {
defer wg.Done()
for i := 0; i
count++
cond.Signal()
mutex.Unlock()
fmt.Println("Goroutine 2: count =", count)
}
}()

wg.Wait()
}

In this example, two goroutines are launched and both increment and decrement a shared count variable. The first goroutine waits on a condition using the Wait method of the Cond type before decrementing the count variable, while the second goroutine signals the condition using the Signal method before incrementing the count variable.

sync is a powerful package in Go that provides a wide range of tools for synchronizing access to shared resources. By using types such as WaitGroup, Mutex, RWMutex, Once, and Pool, and utility functions such as NewCond and NewBarrier, you can ensure that your programs behave correctly and perform efficiently when accessing shared resources.

--

--

Chidozie C. Okafor
Chidozie C. Okafor

Written by Chidozie C. Okafor

Software Engineer & Backend Magician 🎩 | Python, Rust | TypeScript, Node.js | Golang | Kafka & GRPC