· 6 min read

The sync package in Go

provides synchronization primitives to manage concurrent operations.

provides synchronization primitives to manage concurrent operations.

The sync package in Go provides synchronization primitives to manage concurrent operations. Here are the key functionalities with code examples:

1. WaitGroup

A WaitGroup waits for a collection of goroutines to finish.

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // Notify WaitGroup that this goroutine is done
	fmt.Printf("Worker %d starting\n", id)
	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 3; i++ {
		wg.Add(1) // Increment WaitGroup counter
		go worker(i, &wg)
	}

	wg.Wait() // Wait for all goroutines to finish
	fmt.Println("All workers done")
}

2. Mutex

A Mutex ensures exclusive access to shared resources.

package main

import (
	"fmt"
	"sync"
)

var counter int
var mu sync.Mutex

func increment(wg *sync.WaitGroup) {
	defer wg.Done()
	mu.Lock() // Lock the mutex
	counter++
	mu.Unlock() // Unlock the mutex
}

func main() {
	var wg sync.WaitGroup

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go increment(&wg)
	}

	wg.Wait()
	fmt.Println("Final counter value:", counter)
}

3. RWMutex

A RWMutex allows multiple readers or one writer at a time.

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	data  map[string]string
	rwMu  sync.RWMutex
)

func readData(key string, wg *sync.WaitGroup) {
	defer wg.Done()
	rwMu.RLock() // Lock for reading
	value := data[key]
	rwMu.RUnlock() // Unlock after reading
	fmt.Println("Read:", key, value)
}

func writeData(key, value string, wg *sync.WaitGroup) {
	defer wg.Done()
	rwMu.Lock() // Lock for writing
	data[key] = value
	rwMu.Unlock() // Unlock after writing
	fmt.Println("Write:", key, value)
}

func main() {
	data = make(map[string]string)
	var wg sync.WaitGroup

	wg.Add(1)
	go writeData("foo", "bar", &wg)

	wg.Add(2)
	go readData("foo", &wg)
	go readData("foo", &wg)

	wg.Wait()
}

4. Once

Once ensures a function is executed only once, even across multiple goroutines.

package main

import (
	"fmt"
	"sync"
)

var (
	once sync.Once
)

func initialize() {
	fmt.Println("Initializing...")
}

func worker(wg *sync.WaitGroup) {
	defer wg.Done()
	once.Do(initialize) // Ensure initialize() is called only once
	fmt.Println("Worker running")
}

func main() {
	var wg sync.WaitGroup

	for i := 0; i < 3; i++ {
		wg.Add(1)
		go worker(&wg)
	}

	wg.Wait()
}

5. Cond

Cond is a condition variable used to signal between goroutines.

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	mu    sync.Mutex
	cond  *sync.Cond
	ready bool
)

func waitForReady() {
	mu.Lock()
	for !ready {
		cond.Wait() // Wait for the condition to be signaled
	}
	fmt.Println("Ready!")
	mu.Unlock()
}

func setReady() {
	time.Sleep(time.Second)
	mu.Lock()
	ready = true
	cond.Signal() // Signal that the condition is met
	mu.Unlock()
}

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

	go waitForReady()
	go setReady()

	time.Sleep(2 * time.Second)
}

6. Pool

Pool manages a pool of reusable objects to reduce allocation overhead.

package main

import (
	"fmt"
	"sync"
)

type Resource struct {
	ID int
}

func main() {
	var pool = sync.Pool{
		New: func() interface{} {
			return &Resource{ID: 0}
		},
	}

	// Get a resource from the pool
	resource := pool.Get().(*Resource)
	fmt.Println("Resource ID:", resource.ID)

	// Put the resource back into the pool
	pool.Put(resource)

	// Get the same resource again
	resource = pool.Get().(*Resource)
	fmt.Println("Resource ID after reuse:", resource.ID)
}

Summary of Key Functionalities:

  • WaitGroup: Wait for goroutines to finish.
  • Mutex: Ensure exclusive access to shared resources.
  • RWMutex: Allow multiple readers or one writer.
  • Once: Execute a function exactly once.
  • Cond: Signal between goroutines based on conditions.
  • Pool: Manage a pool of reusable objects.

These primitives are essential for writing safe and efficient concurrent Go programs.

If you want to dive even deeper into the sync package and explore additional functionalities or advanced use cases, here are more examples and explanations:


7. Map

sync.Map is a concurrent-safe map designed for use cases where keys are only written once but read many times.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var sm sync.Map

	// Store values
	sm.Store("foo", "bar")
	sm.Store(42, 100)

	// Load values
	if value, ok := sm.Load("foo"); ok {
		fmt.Println("foo:", value)
	}

	// Delete a key
	sm.Delete(42)

	// Range over the map
	sm.Range(func(key, value interface{}) bool {
		fmt.Println(key, ":", value)
		return true // Continue iteration
	})
}

8. Atomic Operations

The sync/atomic package provides low-level atomic memory operations. These are useful for managing simple shared variables without locks.

package main

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

func main() {
	var counter int64
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			atomic.AddInt64(&counter, 1) // Atomic increment
		}()
	}

	wg.Wait()
	fmt.Println("Counter:", atomic.LoadInt64(&counter)) // Atomic read
}

9. Semaphore Pattern Using Channels

While Go doesn’t have a built-in semaphore, you can implement one using buffered channels.

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, sem chan struct{}, wg *sync.WaitGroup) {
	defer wg.Done()
	sem <- struct{}{} // Acquire semaphore
	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
	<-sem // Release semaphore
}

func main() {
	var wg sync.WaitGroup
	sem := make(chan struct{}, 3) // Semaphore with 3 slots

	for i := 1; i <= 10; i++ {
		wg.Add(1)
		go worker(i, sem, &wg)
	}

	wg.Wait()
}

10. Custom Synchronization with Channels

Channels can be used to build custom synchronization mechanisms.

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, done chan bool, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
	done <- true // Signal completion
}

func main() {
	var wg sync.WaitGroup
	done := make(chan bool, 3)

	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go worker(i, done, &wg)
	}

	// Wait for all workers to signal completion
	for i := 0; i < 3; i++ {
		<-done
	}

	wg.Wait()
	fmt.Println("All workers done")
}

11. Error Groups

The errgroup package (from golang.org/x/sync/errgroup) is useful for managing groups of goroutines that can return errors.

package main

import (
	"fmt"
	"golang.org/x/sync/errgroup"
	"time"
)

func worker(id int) error {
	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Second)
	if id == 2 {
		return fmt.Errorf("worker %d failed", id)
	}
	fmt.Printf("Worker %d done\n", id)
	return nil
}

func main() {
	var g errgroup.Group

	for i := 1; i <= 3; i++ {
		id := i
		g.Go(func() error {
			return worker(id)
		})
	}

	if err := g.Wait(); err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("All workers done successfully")
	}
}

12. SingleFlight

The singleflight package (from golang.org/x/sync/singleflight) prevents duplicate function calls for the same key.

package main

import (
	"fmt"
	"golang.org/x/sync/singleflight"
	"sync"
	"time"
)

func fetchData(key string) (string, error) {
	fmt.Println("Fetching data for:", key)
	time.Sleep(time.Second)
	return "data for " + key, nil
}

func main() {
	var g singleflight.Group
	var wg sync.WaitGroup

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			result, err, _ := g.Do("key", func() (interface{}, error) {
				return fetchData("key")
			})
			if err != nil {
				fmt.Println("Error:", err)
			} else {
				fmt.Println("Result:", result)
			}
		}()
	}

	wg.Wait()
}

13. Context with Goroutines

Use context to manage the lifecycle of goroutines.

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func worker(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println("Worker stopped:", ctx.Err())
			return
		default:
			fmt.Println("Working...")
			time.Sleep(500 * time.Millisecond)
		}
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	var wg sync.WaitGroup
	wg.Add(1)
	go worker(ctx, &wg)

	wg.Wait()
	fmt.Println("Main done")
}

Summary of Advanced Functionalities:

  • sync.Map: Concurrent-safe map for read-heavy workloads.
  • Atomic Operations: Low-level atomic memory operations.
  • Semaphore Pattern: Implemented using buffered channels.
  • Custom Synchronization: Using channels for signaling.
  • Error Groups: Manage goroutines that can return errors.
  • SingleFlight: Deduplicate function calls.
  • Context: Manage goroutine lifecycle and cancellation.

These advanced techniques will help you write more robust and efficient concurrent Go programs.

Share:
Back to Blog

Related Posts

View All Posts »
Java enum

Java enum

Java enum is a powerful feature that goes beyond just defining a fixed set of constants.

Singleton Design Pattern

Singleton Design Pattern

The singleton design pattern ensures that a class has only one instance and provides a global point of access to it. It's commonly used for managing shared resources, configurations, or logging mechanisms.