Synchronization

Updated

July 30, 2026

Overview

The sync package provides low-level synchronization primitives for coordinating goroutines.

sync.Mutex

var (
    mu    sync.Mutex
    count int
)

func increment() {
    mu.Lock()
    count++
    mu.Unlock()
}

// With defer
func safe() {
    mu.Lock()
    defer mu.Unlock()
    // Critical section
}

sync.RWMutex

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

func read(key string) string {
    mu.RLock()
    defer mu.RUnlock()
    return data[key]
}

func write(key, value string) {
    mu.Lock()
    defer mu.Unlock()
    data[key] = value
}

sync.WaitGroup

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        work(n)
    }(i)
}

wg.Wait()

sync.Once

var (
    once   sync.Once
    config *Config
)

func getConfig() *Config {
    once.Do(func() {
        config = loadConfig()  // Runs exactly once
    })
    return config
}

sync.Pool

var pool = sync.Pool{
    New: func() any {
        return make([]byte, 1024)
    },
}

func process() {
    buf := pool.Get().([]byte)
    defer pool.Put(buf)
    // Use buf
}

sync.Map

var m sync.Map

m.Store("key", "value")
v, ok := m.Load("key")
m.Delete("key")
m.Range(func(k, v any) bool {
    fmt.Println(k, v)
    return true  // Continue iteration
})

atomic Package

import "sync/atomic"

var counter int64

atomic.AddInt64(&counter, 1)
value := atomic.LoadInt64(&counter)
atomic.StoreInt64(&counter, 0)

Summary

Type Purpose
Mutex Exclusive lock
RWMutex Reader/writer lock
WaitGroup Wait for goroutines
Once Single execution
Pool Object reuse
Map Concurrent map

Worked example

RWMutex cache: many readers, occasional writer.

Save as main.go. Then:

go mod init example
go run .
package main

import (
    "fmt"
    "sync"
)

type Cache struct {
    mu   sync.RWMutex
    data map[string]int
}

func (c *Cache) Get(k string) (int, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[k]
    return v, ok
}

func (c *Cache) Set(k string, v int) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[k] = v
}

func main() {
    c := &Cache{data: map[string]int{}}
    var wg sync.WaitGroup

    // Writers
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            c.Set(fmt.Sprintf("k%d", n%3), n)
        }(i)
    }
    wg.Wait()

    // Readers
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            k := fmt.Sprintf("k%d", n)
            v, ok := c.Get(k)
            fmt.Printf("get %s -> %d ok=%v\n", k, v, ok)
        }(i)
    }
    wg.Wait()
}

Expected output: (values vary; keys present)

get k0 -> ... ok=true
get k1 -> ... ok=true
get k2 -> ... ok=true

More examples

sync.Map for disjoint keys; atomic for a simple counter.

package main

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

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

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m.Store(n%5, n)
            hits.Add(1)
        }(i)
    }
    wg.Wait()

    count := 0
    m.Range(func(_, _ any) bool {
        count++
        return true
    })
    fmt.Println("keys:", count, "hits:", hits.Load())
}

Expected output:

keys: 5 hits: 100

Runnable example

Save as main.go. Then:

go mod init example
go run .
package main

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

func main() {
    // Mutex-protected counter
    var (
        mu    sync.Mutex
        count int
        wg    sync.WaitGroup
    )
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            count++
            mu.Unlock()
        }()
    }
    wg.Wait()
    fmt.Println("mutex count:", count)

    // Once: initialization runs exactly once
    var (
        once   sync.Once
        inited int
    )
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            once.Do(func() {
                inited++
                fmt.Println("once.Do ran")
            })
        }()
    }
    wg.Wait()
    fmt.Println("init count:", inited)

    // atomic counter (no mutex)
    var atomicCount int64
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&atomicCount, 1)
        }()
    }
    wg.Wait()
    fmt.Println("atomic count:", atomic.LoadInt64(&atomicCount))

    // Pool: reuse a buffer
    pool := sync.Pool{
        New: func() any {
            return make([]byte, 8)
        },
    }
    buf := pool.Get().([]byte)
    copy(buf, []byte("go-pool!"))
    fmt.Println("pool buffer:", string(buf))
    pool.Put(buf)
}

Expected output:

mutex count: 1000
once.Do ran
init count: 1
atomic count: 1000
pool buffer: go-pool!

What to notice: Without mu.Lock / atomic, the counters would race. sync.Once guarantees single execution even under concurrent callers. sync.Pool is for short-lived reuse—never assume a put buffer is still yours after Put.

Try next: Remove the mutex around count++ and run go run -race . to watch the race detector fire.