100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Go

The sync Package in Go

The sync package provides primitives like WaitGroup, Mutex, and Once for coordinating goroutines and protecting shared state.

ConcurrencyIntermediate9 min readJul 8, 2026
Analogies

Introduction

While channels are Go's preferred way to coordinate goroutines through communication, sometimes you need lower-level synchronization primitives to protect shared memory directly or wait for a set of tasks to finish. The standard library's sync package provides these tools: sync.WaitGroup for waiting on a group of goroutines, sync.Mutex and sync.RWMutex for guarding shared data from concurrent access, and sync.Once for guaranteeing that initialization code runs exactly one time.

🏏

Cricket analogy: While channels are like passing the ball between fielders to communicate, sometimes you need a direct lock on the scoreboard itself so two scorers don't write over each other; sync.Mutex guards that shared scoreboard, sync.WaitGroup waits for all fielders to reach position, and sync.Once ensures the pitch is only rolled once before play.

Syntax

go
var wg sync.WaitGroup
wg.Add(1)      // register one goroutine to wait for
go func() {
    defer wg.Done()   // signal completion
}()
wg.Wait()       // block until all Add'd goroutines call Done

var mu sync.Mutex
mu.Lock()
// critical section: access shared data
mu.Unlock()

var once sync.Once
once.Do(func() {
    // runs only on the first call, ever
})

Explanation

sync.WaitGroup maintains an internal counter: Add(n) increases it, Done() decrements it (typically via defer), and Wait() blocks until the counter reaches zero — a clean way to wait for many goroutines without polling. sync.Mutex provides mutual exclusion: Lock() acquires exclusive access and Unlock() releases it, ensuring only one goroutine at a time executes the critical section between them; sync.RWMutex extends this by allowing many concurrent readers (RLock/RUnlock) but only one writer (Lock/Unlock) at a time, which helps read-heavy workloads. sync.Once guarantees that a function passed to Do executes exactly once across all goroutines, no matter how many times Do is called — ideal for lazy, thread-safe initialization such as setting up a singleton or loading configuration.

🏏

Cricket analogy: sync.WaitGroup is like a captain counting fielders back to the pavilion, Add for each one sent out, Done as each returns, Wait blocking until all are in; sync.Mutex is like one scorer allowed to write at a time, RWMutex lets many spectators read the scoreboard but only one scorer edit it, and sync.Once is like the pitch being rolled exactly once before the match no matter how many groundstaff ask.

Example

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    var wg sync.WaitGroup
    counter := 0

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            counter++
            mu.Unlock()
        }()
    }

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

Output

Final counter: 100 Without the mutex protecting counter++, concurrent increments from 100 goroutines could race and produce a value less than 100 (a data race). The mutex ensures each increment is atomic relative to the others, so the final result is always correct.

🏏

Cricket analogy: Just as a match without a designated scorer could let two people write conflicting totals on the board, a race between 100 unguarded increments could undercount; the mutex is like appointing one official scorer so every run is recorded correctly, giving the true final count of 100.

Key Takeaways

  • sync.WaitGroup coordinates waiting for a group of goroutines via Add, Done, and Wait.
  • sync.Mutex guards shared state with Lock/Unlock so only one goroutine accesses it at a time.
  • sync.RWMutex allows multiple concurrent readers but exclusive writers.
  • sync.Once ensures initialization code runs exactly once, safely across goroutines.
  • Always call Done() (often via defer) to avoid a WaitGroup that never unblocks.
  • Run 'go run -race' to detect unprotected concurrent access to shared data.

Practice what you learned

Was this page helpful?

Topics covered

#Go#GoProgrammingStudyNotes#Programming#TheSyncPackageInGo#Sync#Package#Syntax#Explanation#StudyNotes#SkillVeris