Inside Go’s sync.WaitGroup: The Story Behind Goroutine Synchronization
James Reed
Infrastructure Engineer · Leapcell

In - depth Analysis of sync.WaitGroup Principles and Applications
1. Overview of the Core Functions of sync.WaitGroup
1.1 Synchronization Needs in Concurrent Scenarios
In the concurrent programming model of the Go language, when a complex task needs to be broken down into multiple independent subtasks to be executed in parallel, the scheduling mechanism of goroutines may cause the main goroutine to exit early while the subtasks have not been completed. At this time, a mechanism is needed to ensure that the main goroutine waits for all subtasks to be completed before continuing to execute the subsequent logic. sync.WaitGroup is a core tool designed to solve such goroutine synchronization problems.
1.2 Basic Usage Paradigm
Definition of Core Methods
- Add(delta int): Sets or adjusts the number of subtasks to wait for.
delta
can be positive or negative (a negative value means reducing the number of waits). - Done(): Called when a subtask is completed, which is equivalent to
Add(-1)
. - Wait(): Blocks the current goroutine until all the subtasks to be waited for are completed.
Typical Code Example
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(2) // Set the number of subtasks to wait for as 2 go func() { defer wg.Done() // Mark when the subtask is completed fmt.Println("Subtask 1 executed") }() go func() { defer wg.Done() fmt.Println("Subtask 2 executed") }() wg.Wait() // Block until all subtasks are completed fmt.Println("The main goroutine continues to execute") }
Explanation of Execution Logic
- The main goroutine declares that it needs to wait for 2 subtasks through
Add(2)
. - Subtasks notify completion through
Done()
, and internally callAdd(-1)
to reduce the counter. Wait()
continues to block until the counter reaches zero, and the main goroutine resumes execution.
2. Source Code Implementation and Data Structure Analysis (Based on Go 1.17.10)
2.1 Memory Layout and Data Structure Design
type WaitGroup struct { noCopy noCopy // A marker to prevent the structure from being copied state1 [3]uint32 // Composite data storage area }
Field Analysis
-
noCopy Field
Through thego vet
static inspection mechanism of the Go language,WaitGroup
instances are prohibited from being copied to avoid state inconsistency caused by copying. This field is essentially an unused structure, only used to trigger compile - time checks. -
state1 Array
It uses a compact memory layout to store three types of core data, compatible with the memory alignment requirements of 32 - bit and 64 - bit systems:- 64 - bit System:
state1[0]
: Counter, records the number of remaining subtasks to be completed.state1[1]
: Waiter count, records the number of goroutines that have calledWait()
.state1[2]
: Semaphore, used for blocking and waking up between goroutines.
- 32 - bit System:
state1[0]
: Semaphore.state1[1]
: Counter.state1[2]
: Waiter count.
- 64 - bit System:
Memory Alignment Optimization
By combining counter
and waiter
into a 64 - bit integer (the high 32 bits are counter
, and the low 32 bits are waiter
), natural alignment is ensured on 64 - bit systems, improving the efficiency of atomic operations. On 32 - bit systems, the position of the semaphore is adjusted to ensure the address alignment of 64 - bit data blocks.
2.2 Implementation Details of Core Methods
2.2.1 state() Method: Data Extraction Logic
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) { // Determine the memory alignment method if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 { // 64 - bit alignment: the first two uint32s form the state, and the third is the semaphore return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2] } else { // 32 - bit alignment: the last two uint32s form the state, and the first is the semaphore return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0] } }
- Dynamically determine the distribution of data in the array through the alignment characteristics of the pointer address.
- Use
unsafe.Pointer
to achieve underlying memory access and ensure cross - platform compatibility.
2.2.2 Add(delta int) Method: Counter Update Logic
func (wg *WaitGroup) Add(delta int) { statep, semap := wg.state() // Atomically update the counter (high 32 bits) state := atomic.AddUint64(statep, uint64(delta)<<32) v := int32(state >> 32) // Extract the counter w := uint32(state) // Extract the waiter count // The counter cannot be negative if v < 0 { panic("sync: negative WaitGroup counter") } // Prohibit calling Add concurrently when Wait is executing if w != 0 && delta > 0 && v == int32(delta) { panic("sync: WaitGroup misuse: Add called concurrently with Wait") } // When the counter is zero and there are waiters, release the semaphore if v == 0 && w != 0 { *statep = 0 // Reset the state for ; w > 0; w-- { runtime_Semrelease(semap, false, 0) // Wake up the waiting goroutines } } }
- Core Logic: Ensure the thread safety of counter updates through atomic operations. When the counter is zero and there are waiting goroutines, wake up all waiters through the semaphore release mechanism.
- Exception Handling: Strictly check for illegal operations such as negative counters and concurrent calls to avoid program logic errors.
2.2.3 Wait() Method: Blocking and Wake - up Mechanism
func (wg *WaitGroup) Wait() { statep, semap := wg.state() for { state := atomic.LoadUint64(statep) // Atomically read the state v := int32(state >> 32) w := uint32(state) if v == 0 { // If the counter is 0, return directly return } // Safely increase the waiter count using a CAS operation if atomic.CompareAndSwapUint64(statep, state, state+1) { runtime_Semacquire(semap) // Block the current goroutine and wait for the semaphore to be released // Check the state consistency if *statep != 0 { panic("sync: WaitGroup is reused before previous Wait has returned") } return } } }
- Spin Waiting: Ensure the safe increment of the waiter count through loop CAS operations to avoid race conditions.
- Semaphore Blocking: Call
runtime_Semacquire
to enter the blocking state until theAdd
orDone
operation releases the semaphore to wake up the goroutine.
2.2.4 Done() Method: Quick Counter Decrement
func (wg *WaitGroup) Done() { wg.Add(-1) // Equivalent to decrementing the counter by 1 }
3. Usage Specifications and Precautions
3.1 Key Usage Principles
-
Order Requirements
TheAdd
operation must be completed before theWait
call to avoid the failure of the waiting logic caused by the uninitialized counter. -
Count Consistency
The number ofDone
calls must be consistent with the initial count set byAdd
. Otherwise, the counter may not be able to reach zero, causing permanent blocking. -
Prohibition of Concurrent Operations
- It is strictly forbidden to call
Add
concurrently during the execution ofWait
, otherwise a panic will be triggered. - When reusing
WaitGroup
, ensure that the previousWait
has returned to avoid state confusion.
- It is strictly forbidden to call
3.2 Typical Error Scenarios
Error Operation | Consequence | Example Code |
---|---|---|
Negative Counter | panic | wg.Add(-1) (when the initial count is 0) |
Concurrent Calling of Add and Wait | panic | The main goroutine calls Wait while the subtask calls Add |
Unpaired Calling of Done | Permanent Blocking | After wg.Add(1) , Done is not called |
4. Summary
sync.WaitGroup
is a basic tool for handling goroutine synchronization in Go language concurrent programming. Its design fully reflects the engineering practice principles such as memory alignment optimization, atomic operation safety, and error checking. By deeply understanding its data structure and implementation logic, developers can use this tool more safely and efficiently and avoid common pitfalls in concurrent scenarios. In practical applications, it is necessary to strictly follow the specifications such as count matching and sequential calling to ensure the correctness and stability of the program.
Leapcell: The Best of Serverless Web Hosting
Finally, recommend a platform that is most suitable for deploying Go services: Leapcell
🚀 Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
🌍 Deploy Unlimited Projects for Free
Only pay for what you use—no requests, no charges.
⚡ Pay - as - You - Go, No Hidden Costs
No idle fees, just seamless scalability.
🔹 Follow us on Twitter: @LeapcellHQ