Go: Go Synchronization Primitives

Channels are used for communication between goroutines, and the sync package is used to protect shared state—Go provides a complete toolkit for concurrent programming.

When multiple goroutines need to read from or write to the same متغير, synchronization primitives are required to ensure safety. In this lesson, you'll learn all the core tools and selection strategies of Go's sync package.

1. You will learn



2. A True Story of a High-Concurrency Engineer

(1) Problem: 100 goroutines write to the counter simultaneously, causing the data to become completely corrupted

Charlie is a واجهة خلفية engineer for an e-commerce platform, and he needs to count the number of orders per second:

"100 goroutines were processing orders simultaneously, and each one incremented the counter by 1 after completing an order. On the first day after launch, the data was completely off—the dashboard showed 1,000 orders per second, but there were only 300 orders in the قاعدة بيانات. My boss asked me, 'Where did the money for those 700 orders go?'"

He opened the code:

GO
// Bad code: concurrent write to shared variable, no lock
var counter int

func processOrder(orderID string) {
    // ... process order
    counter++  // Non-atomic operation! Equivalent to:
               // temp = counter
               // temp = temp + 1
               // counter = temp  <- 3 goroutines executing this step simultaneously = chaos
}

counter++ is not an atomic operation—it breaks down into three steps: read, increment by one, and write. If two goroutines simultaneously read counter=10, increment it by one, and then write it back, the result will be counter=11 instead of 12—this is a classic example of a data race.

(2) Solution in Go: Mutex

GO
// counter.go
package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    value int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    c.value++ // Protected by the lock; only one goroutine can execute at a time
    c.mu.Unlock()
}

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

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

    // 100 goroutines incrementing simultaneously
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    wg.Wait()
    fmt.Printf("Final value: %d (expected 100)\n", counter.Value())
}

Output:

TEXT 📖 Display only
Final value: 100 (expected 100)

(3) Returns: Locked vs. Unlocked

Situation Result Data Race
Lock-free concurrency 95/100/97 (random) ✅ Yes (race condition detected)
Mutex Protection 100 ❌ None
atomic operation 100 ❌ None
💡 Tip: go run -race main.go can detect data races. If concurrent access to shared variables is not protected by locks, the -race option will issue a warning at runtime. It is recommended to always enable -race in CI/CD.



3. sync.Mutex Mutex

(1) Mutex Basics

GO
package main

import (
    "fmt"
    "sync"
)

type Account struct {
    mu      sync.Mutex
    balance float64
}

func (a *Account) Deposit(amount float64) {
    a.mu.Lock()
    a.balance += amount
    a.mu.Unlock()
}

func (a *Account) Withdraw(amount float64) error {
    a.mu.Lock()
    defer a.mu.Unlock()
    if a.balance < amount {
        return fmt.Errorf("insufficient balance: have %.2f, need %.2f", a.balance, amount)
    }
    a.balance -= amount
    return nil
}

func (a *Account) Balance() float64 {
    a.mu.Lock()
    defer a.mu.Unlock()
    return a.balance
}

func main() {
    acc := Account{balance: 1000}
    var wg sync.WaitGroup

    // 10 concurrent deposits
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            acc.Deposit(100)
        }()
    }
    wg.Wait()

    fmt.Printf("Balance: %.2f\n", acc.Balance())
}
GO
// Recommended: Lock then immediately defer Unlock
func (a *Account) SafeMethod() {
    a.mu.Lock()
    defer a.mu.Unlock()
    // All operations are within the lock
    a.balance += 100
    a.balance -= 50
    // Even if there is a panic in between, defer ensures Unlock
}

// Not recommended: manual Unlock
func (a *Account) UnsafeMethod() {
    a.mu.Lock()
    a.balance += 100
    a.mu.Unlock() // If there is a return or panic in between, Unlock won't be called → deadlock
}
▶ Try it Yourself
🔥 Common Mistake: After Lock, you must call Unlock. Always use defer a.mu.Unlock() instead of manually calling Unlock at the end—if there are multiple return paths, forgetting to call Unlock in any one of them will result in a deadlock.



4. sync.RWMutex Read-Write Lock

(1) Principles of Read-Write Locks

GO
package main

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

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

func NewCache() *Cache {
    return &Cache{data: make(map[string]string)}
}

// Read operation: RLock (concurrent reads, non-exclusive)
func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[key]
    return v, ok
}

// Write operation: Lock (exclusive, blocks all reads and writes)
func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

func main() {
    cache := NewCache()
    var wg sync.WaitGroup

    // Multiple concurrent reads (RLock is non-exclusive)
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 3; j++ {
                if v, ok := cache.Get("key"); ok {
                    fmt.Printf("Reader %d: %s\n", id, v)
                }
                time.Sleep(10 * time.Millisecond)
            }
        }(i)
    }

    // One writer
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(5 * time.Millisecond)
        cache.Set("key", "value")
        fmt.Println("Writer: write complete")
    }()

    wg.Wait()
}

▶ Example: Performance Comparison of Mutex vs. RWMutex

GO
package main

import (
    "sync"
    "testing"
)

type DataStore struct {
    mu    sync.RWMutex
    value int
}

func BenchmarkMutexRead(b *testing.B) {
    store := DataStore{}
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            store.mu.Lock()
            _ = store.value
            store.mu.Unlock()
        }
    })
}

func BenchmarkRWMutexRead(b *testing.B) {
    store := DataStore{}
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            store.mu.RLock()
            _ = store.value
            store.mu.RUnlock()
        }
    })
}

// go test -bench=. -benchmem
▶ Try it Yourself

(3) Mutex vs RWMutex

Feature Mutex RWMutex
Read and Read Mutual Exclusion Non-Mutual Exclusion
Read and Write Mutual Exclusion Mutual Exclusion
Write and Write Mutual Exclusion Mutual Exclusion
Use Cases Read-write ratio 1:1 Reads far exceed writes (e.g., cache)
Read Performance Slow (serialized) Fast (concurrent reads)


5. sync.Once: Execute once

(1) Once Basics

GO
package main

import (
    "fmt"
    "sync"
)

var (
    config     map[سلسلة]سلسلة
    configOnce sync.Once
)

func loadConfig() {
    configOnce.Do(func() {
        fmt.Println("Initializing config...")
        config = map[سلسلة]سلسلة{
            "host": "localhost",
            "port": "8080",
        }
    })
}

func main() {
    var wg sync.WaitGroup
    // 100 goroutines call simultaneously, but init executes only once
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            loadConfig()
        }()
    }
    wg.Wait()
    fmt.Printf("config: %v\n", config)
}

Output:

TEXT 📖 Display only
Initializing config...
config: map[host:localhost port:8080]

(2) sync.Once vs. manual flags

Method Thread-safe Code Size
if !initialized { ... } ❌ Data race Small
sync.Once ✅ Go guarantees Minimal
init() دالة ✅ Executed when the package is loaded Minimal
💡 Tip: sync.Once ensures that the دالة inside Do is executed only once, even if it is called simultaneously by 1,000 goroutines. Its internal implementation uses atomic operations and mutexes, making it very efficient.



6. sync.Map: A Concurrency-Safe Map

(1) sync.Map Basics

GO
package main

import (
    "fmt"
    "sync"
)

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

    // Concurrent writes
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m.Store(fmt.Sprintf("key-%d", n), n*100)
        }(i)
    }
    wg.Wait()

    // Read
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%s = %d\n", key, value)
        return true
    })

    // LoadOrStore: returns if exists, stores if not
    actual, loaded := m.LoadOrStore("key-0", 999)
    fmt.Printf("LoadOrStore: actual=%d, loaded=%v\n", actual, loaded)
}

▶ Example: sync.Map vs. map+Mutex

GO
package main

import (
    "sync"
    "testing"
)

// map + Mutex
type MutexMap struct {
    mu    sync.Mutex
    items map[سلسلة]int
}

func (m *MutexMap) Store(key سلسلة, value int) {
    m.mu.Lock()
    m.items[key] = value
    m.mu.Unlock()
}

func (m *MutexMap) Load(key سلسلة) (int, bool) {
    m.mu.Lock()
    v, ok := m.items[key]
    m.mu.Unlock()
    return v, ok
}

func BenchmarkMutexMap(b *testing.B) {
    m := &MutexMap{items: make(map[سلسلة]int)}
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            m.Store("key", 1)
            m.Load("key")
        }
    })
}

func BenchmarkSyncMap(b *testing.B) {
    var m sync.Map
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            m.Store("key", 1)
            m.Load("key")
        }
    })
}

// go test -bench=. -benchmem
▶ Try it Yourself

(3) Use Cases for sync.Map

Scenario Recommendation Reason
Key written once sync.Map Read optimization (few writes, many reads)
Frequent key updates map+Mutex sync.Map writes slightly slower
Key set growing sync.Map Avoid locking the entire map
Simple scenario map+Mutex More intuitive
High-performance requirements Specific benchmark Determined by actual testing


7. sync/atomic: Atomic Operations

(1) Atomic Basics

GO
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var counter int64

    // Atomic increment
    newVal := atomic.AddInt64(&counter, 1)
    fmt.Printf("AddInt64: %d\n", newVal)

    // Atomic read
    val := atomic.LoadInt64(&counter)
    fmt.Printf("LoadInt64: %d\n", val)

    // Atomic write
    atomic.StoreInt64(&counter, 100)

    // CAS (Compare And Swap)
    swapped := atomic.CompareAndSwapInt64(&counter, 100, 200)
    fmt.Printf("CAS: swapped=%v, val=%d\n", swapped, atomic.LoadInt64(&counter))
}

▶ Example: Atomic Counters vs. Mutexes

GO 📖 Display only
package main

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

type AtomicCounter struct {
    value int64
}

func (c *AtomicCounter) Increment() {
    atomic.AddInt64(&c.value, 1)
}

func (c *AtomicCounter) Value() int64 {
    return atomic.LoadInt64(&c.value)
}

type MutexCounter struct {
    mu    sync.Mutex
    value int
}

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

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

func main() {
    var wg sync.WaitGroup
    n := 100000

    // Atomic counter
    atomicStart := time.Now()
    var ac AtomicCounter
    for i := 0; i < n; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            ac.Increment()
        }()
    }
    wg.Wait()
    fmt.Printf("Atomic: %d (%v)\n", ac.Value(), time.Since(atomicStart))

    // Mutex counter
    muStart := time.Now()
    var mc MutexCounter
    for i := 0; i < n; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mc.Increment()
        }()
    }
    wg.Wait()
    fmt.Printf("Mutex: %d (%v)\n", mc.Value(), time.Since(muStart))
}
56 logic lines (exceeds 40-line limit, display only)

(3) Common atomic functions

Operation Function Purpose
Add/Subtract AddInt64, AddUint32 Counter
Read LoadInt64, LoadPointer Safe read
Write StoreInt64, StorePointer Safe write
CAS CompareAndSwapInt64 Optimistic lock
Swap SwapInt64 Atomic swap


8. race detector

▶ Example: Data Race Detection

GO
// race_example.go
package main

import (
    "fmt"
    "sync"
)

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

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // Data race!
        }()
    }

    wg.Wait()
    fmt.Println(counter)
}
▶ Try it Yourself
BASH
$ go run -race race_example.go
==================
WARNING: DATA RACE
Read at 0x00c0000... by goroutine X
  main.main.func1()
      race_example.go:14 +0x...

Previous write at 0x00c0000... by goroutine Y
  main.main.func1()
      race_example.go:14 +0x...
==================


9. sync.Pool Object Pool

▶ Example: Object Pool

GO
package main

import (
    "fmt"
    "sync"
)

type User struct {
    Name string
    Age  int
}

var userPool = sync.Pool{
    New: func() interface{} {
        return &User{}
    },
}

func main() {
    // Get from pool (avoids allocating a new object each time)
    u := userPool.Get().(*User)
    u.Name = "Alice"
    u.Age = 28
    fmt.Printf("Using: %+v\n", u)

    // Put back into the pool after use
    userPool.Put(u)

    // Next Get reuses the previous object
    u2 := userPool.Get().(*User)
    fmt.Printf("Reusing: %+v\n", u2)
}
▶ Try it Yourself

10. Complete Example: High-Concurrency Counter

GO
// concurrent_counter.go
package main

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

// ---------- 3 counter variants ----------

type Counter interface {
    Increment()
    Value() int64
}

// AtomicCounter
type AtomicCounter struct {
    value int64
}

func (c *AtomicCounter) Increment() {
    atomic.AddInt64(&c.value, 1)
}

func (c *AtomicCounter) Value() int64 {
    return atomic.LoadInt64(&c.value)
}

// MutexCounter
type MutexCounter struct {
    mu    sync.Mutex
    value int64
}

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

func (c *MutexCounter) Value() int64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

// ChannelCounter
type ChannelCounter struct {
    ch    chan struct{}
    value int64
    done  chan struct{}
}

func NewChannelCounter() *ChannelCounter {
    c := &ChannelCounter{
        ch:   make(chan struct{}, 100),
        done: make(chan struct{}),
    }
    go func() {
        for range c.ch {
            c.value++
        }
        close(c.done)
    }()
    return c
}

func (c *ChannelCounter) Increment() {
    c.ch <- struct{}{}
}

func (c *ChannelCounter) Value() int64 {
    return atomic.LoadInt64(&c.value)
}

func (c *ChannelCounter) Close() {
    close(c.ch)
    <-c.done
}

// ---------- Benchmark ----------

func benchmarkCounter(c Counter, goroutines, increments int) time.Duration {
    var wg sync.WaitGroup
    start := time.Now()

    for g := 0; g < goroutines; g++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for i := 0; i < increments; i++ {
                c.Increment()
            }
        }()
    }

    wg.Wait()
    return time.Since(start)
}

func main() {
    const (
        goroutines = 100
        increments = 10000
    )

    fmt.Printf("Benchmark: %d goroutines, each executing %d Increments\n\n", goroutines, increments)

    // 1. Atomic counter
    ac := &AtomicCounter{}
    atomicTime := time.Now()
    atomicDone := make(chan struct{})
    go func() {
        defer close(atomicDone)
        var wg sync.WaitGroup
        for g := 0; g < goroutines; g++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                for i := 0; i < increments; i++ {
                    ac.Increment()
                }
            }()
        }
        wg.Wait()
    }()
    <-atomicDone
    fmt.Printf("AtomicCounter: %d (expected %d) — %v\n",
        ac.Value(), goroutines*increments, time.Since(atomicTime))

    // 2. Mutex counter
    mc := &MutexCounter{}
    mutexTime := time.Now()
    mutexDone := make(chan struct{})
    go func() {
        defer close(mutexDone)
        var wg sync.WaitGroup
        for g := 0; g < goroutines; g++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                for i := 0; i < increments; i++ {
                    mc.Increment()
                }
            }()
        }
        wg.Wait()
    }()
    <-mutexDone
    fmt.Printf("MutexCounter: %d (expected %d) — %v\n",
        mc.Value(), goroutines*increments, time.Since(mutexTime))

    // 3. Channel counter
    cc := NewChannelCounter()
    channelTime := time.Now()
    channelDone := make(chan struct{})
    go func() {
        defer close(channelDone)
        var wg sync.WaitGroup
        for g := 0; g < goroutines; g++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                for i := 0; i < increments; i++ {
                    cc.Increment()
                }
            }()
        }
        wg.Wait()
        cc.Close()
    }()
    <-channelDone
    fmt.Printf("ChannelCounter: %d (expected %d) — %v\n",
        cc.Value(), goroutines*increments, time.Since(channelTime))

    fmt.Println("\nConclusion: Atomic > Mutex > Channel (performance from high to low)")
}
100%
sequenceDiagram
    participant G1 as Goroutine 1
    participant G2 as Goroutine 2
    participant G3 as Goroutine 3
    participant M as Mutex
    
    G1->>M: Lock()
    Note over G1,M: G1 holds the lock
    G2->>M: Lock() blocked
    G3->>M: Lock() blocked
    G1->>M: Unlock()
    M-->>G2: wake up
    M-->>G3: still blocked
    G2->>M: Lock() success
    Note over G2,M: G2 holds the lock
    G2->>M: Unlock()
    M-->>G3: wake up
    G3->>M: Lock() success
🔥 Common Mistake: sync.Mutex cannot be copied. If you pass a struct containing a Mutex by value (rather than by pointer), the Mutex's state will be copied—resulting in unexpected unlocking behavior. Always pass it by pointer (*Counter).


❓ FAQ

Q How do I choose between a Mutex and an RWMutex?
A If reads far outnumber writes (>10:1) → RWMutex; if reads and writes are roughly equal or writes outnumber reads → Mutex. The RLock of an RWMutex allows concurrent reads, but the Lock itself mutualizes all reads and writes. Choosing the wrong one will reduce performance.
Q How do I use sync.Once?
A sync.Once.Do(f) ensures that f is executed only once, even if 1,000 goroutines call it simultaneously. Typical uses: lazy-loading configurations, the singleton pattern, and resources that need to be initialized only once. It is implemented internally using atomic operations and a mutex.
Q In what scenarios is sync.Map suitable?
A The official documentation recommends two scenarios: (1) when a key is written only once but read multiple times (ذاكرة مخبأة); (2) when multiple goroutines read, write, or iterate over different sets of keys. Otherwise, using a map with an RWMutex is simpler and offers comparable performance.
Q In what scenarios is atomic appropriate?
A Simple counters, حالة flags, and CAS (Compare And Swap) optimistic locking. atomic is 10 to 100 times faster than a mutex, but it can only be used with basic types such as int, uint, and pointer. For complex logic, use a mutex instead.
Q How do I use the race detector?
A go run -race main.go or go test -race ./.... It detects data races at runtime—if two goroutines concurrently access the same متغير and at least one of them is a write operation, a warning will be issued. It is recommended to keep it enabled for CI/CD.
Q When should sync.Pool be used?
A When you need to frequently allocate and deallocate temporary objects (such as buffers for JSON decoding or Protobuf messages). The pool caches allocated objects to reduce the load on the garbage collector. Note: Objects in the pool may be reclaimed by the garbage collector at any time.
Q Is a Mutex reentrant?
A No. Go's sync.Mutex is a non-reentrant lock—if the same goroutine calls Lock twice in succession, it will result in a deadlock. If you need to acquire the lock again within the same goroutine, refactor the code to use sync.Mutex with recursive calls, or use defer to ensure the lock is properly released.
Q Does the order of Lock and RLock matter?
A It matters a great deal. RWMutex does not allow calling Lock during an RLock—this will cause a deadlock. It also does not allow calling RLock during a Lock (writer-first). Recommendation: Use only Lock for write operations and only RLock for read operations; do not mix them.

📖 Summary


📝 Exercises

  1. Basic Problem (Difficulty ⭐): Use a mutex to protect concurrent reads and writes to a map[سلسلة]int. Launch 10 writer goroutines to write data and 10 reader goroutines to read data, and verify that there is no race condition (the -race test passes).

  2. Advanced Problem (Difficulty ⭐⭐): Compare the performance of four types of counters (atomic, Mutex, RWMutex, and channel) by measuring the time taken and verifying the correctness of 100 goroutines performing 10,000 increment operations. Use -race to verify that there are no data races.

  3. Challenge (Difficulty ⭐⭐⭐): Implement a high-concurrency ذاكرة مخبأة with many reads and few writes. Requirements: (1) Use RWMutex for protection; (2) Support Get/Set/Delete/Range operations; (3) Support TTL expiration (time.Now().After(expireAt)); (4) Block all read operations during write operations, but allow concurrent read operations; (5) Pass the -race test.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏