sync and sync/atomic

Updated

September 8, 2026

sync and sync/atomic

Overview

When goroutines share memory, the stdlib gives two complementary tools:

Package Style
sync Mutexes, WaitGroups, Once, Cond, Map, Pool
sync/atomic Lock-free counters and pointers for simple cases

Prefer channels for ownership transfer and mutexes for shared structures. Concurrency design: Part 07.

WaitGroup

var wg sync.WaitGroup
for _, job := range jobs {
    wg.Add(1)
    go func(j Job) {
        defer wg.Done()
        process(j)
    }(job)
}
wg.Wait()

Call Add before spawning; never negative. Prefer wg.Go (Go 1.25+) when available:

wg.Go(func() { process(job) })

Mutex and RWMutex

type Cache struct {
    mu sync.Mutex
    m  map[string]string
}

func (c *Cache) Get(k string) (string, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()
    v, ok := c.m[k]
    return v, ok
}

RWMutex helps read-heavy maps; measure before assuming it wins.

Once

var once sync.Once
var client *http.Client

func Client() *http.Client {
    once.Do(func() {
        client = &http.Client{Timeout: 10 * time.Second}
    })
    return client
}

Pool

var bufPool = sync.Pool{
    New: func() any { return make([]byte, 32*1024) },
}
b := bufPool.Get().([]byte)
defer bufPool.Put(b[:cap(b)]) // reset length if you resliced

Use for high-churn buffers; do not assume pooled items are zeroed.

Map

sync.Map is specialized (append-only keys, many disjoint keys). For ordinary caches, a mutex + map is clearer.

Cond (advanced)

mu := sync.Mutex{}
cond := sync.NewCond(&mu)
// Waiters: cond.Wait(); Signal/Broadcast after state change under the same mutex

See pipelines / Cond.

atomic

var hits atomic.Int64
hits.Add(1)
fmt.Println(hits.Load())

var flag atomic.Bool
flag.Store(true)

Typed atomics (atomic.Int64, Bool, Pointer[T]) are preferred over AddInt64 free functions.

Compare-and-swap

var p atomic.Pointer[Config]
p.Store(&Config{V: 1})
for {
    old := p.Load()
    next := *old
    next.V++
    if p.CompareAndSwap(old, &next) {
        break
    }
}

Race detector

go test -race ./...
go run -race .

If the race detector complains, fix the ownership story — do not “hope” atomics alone fix complex structs.

Runnable example

go mod init example
go run .
package main

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

func main() {
    var hits atomic.Int64
    var wg sync.WaitGroup
    var mu sync.Mutex
    seen := make(map[int]bool)

    for i := 0; i < 8; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            hits.Add(1)
            mu.Lock()
            seen[id] = true
            mu.Unlock()
        }(i)
    }
    wg.Wait()

    fmt.Println("hits", hits.Load())
    fmt.Println("unique", len(seen))

    var once sync.Once
    once.Do(func() { fmt.Println("init once") })
    once.Do(func() { fmt.Println("never again") })
}

Expected output:

hits 8
unique 8
init once

What to notice: - Counters can be atomic; maps still need a mutex (or channels). - Once runs exactly one successful init path. - Run with -race to validate this pattern under the detector.

Try next: Replace the mutex map with a channel that owns the map in a single goroutine (share-by-communication).