sync.WaitGroup
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 64 of 107.
In the Goroutines Basics lesson, we used time.Sleep to wait for goroutines to finish—a fragile approach. sync.WaitGroup provides a proper way to wait for a collection of goroutines to complete their work.
A WaitGroup works like a counter. You increment it before starting a goroutine, decrement it when the goroutine finishes, and block until the counter reaches zero:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // increment counter
go func(id int) {
defer wg.Done() // decrement when done
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // block until counter is 0
fmt.Println("All workers complete")
}The three key methods are Add(n) to increase the counter by n, Done() to decrease it by one (equivalent to Add(-1)), and Wait() to block until the counter reaches zero. Using defer wg.Done() ensures the counter decrements even if the goroutine panics.
A common mistake is calling Add inside the goroutine instead of before it. This creates a race condition where Wait might return before all goroutines are registered:
// Wrong - race condition
go func() {
wg.Add(1) // might run after Wait()
defer wg.Done()
}()
// Correct - Add before launching
wg.Add(1)
go func() {
defer wg.Done()
}()WaitGroups are often embedded in structs to coordinate concurrent operations, making them essential for building thread-safe types in Go.
Challenge
EasyLet's build a download manager that coordinates multiple concurrent file downloads using sync.WaitGroup. Your manager will track when all downloads complete without relying on arbitrary sleep timers.
You'll organize your code across two files:
downloader.go: Define your download coordination logic.Create a
Downloadstruct withID(int),Filename(string), andSize(int, representing KB) fields.Implement a method
Process(wg *sync.WaitGroup)onDownloadthat simulates downloading by sleeping for a duration based on the file size (useSize * 10milliseconds). When the download completes, print:Downloaded: [Filename] ([Size]KB). Usedefer wg.Done()to ensure the WaitGroup counter decrements properly.Create a function
StartDownloads(downloads []Download) intthat launches all downloads concurrently using goroutines and waits for them all to complete. Remember to callwg.Add(1)before launching each goroutine, not inside it. Return the total size of all downloads combined.main.go: Read download information and orchestrate the concurrent downloads.Read the number of downloads, then for each download read its ID, filename, and size. Create the downloads and pass them to
StartDownloads. After all downloads complete, print:All downloads complete: [total]KB total
The following inputs will be provided:
- Line 1: Number of downloads (integer)
- Following lines: For each download, three lines - the ID (integer), filename (string), and size in KB (integer)
For example, given:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10Your output should show downloads completing (smaller files finish first due to shorter sleep times), followed by the summary:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB totalThe key difference from using time.Sleep is that wg.Wait() blocks until all goroutines signal completion with Done(), giving you precise synchronization regardless of how long each download takes.
Cheat sheet
A sync.WaitGroup provides a proper way to wait for goroutines to complete. It works like a counter that tracks running goroutines.
The three key methods are:
Add(n)- increases the counter by nDone()- decreases the counter by one (equivalent toAdd(-1))Wait()- blocks until the counter reaches zero
Basic usage pattern:
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // increment before starting goroutine
go func(id int) {
defer wg.Done() // decrement when done
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // block until all goroutines complete
fmt.Println("All workers complete")Using defer wg.Done() ensures the counter decrements even if the goroutine panics.
Important: Always call Add before launching the goroutine, not inside it, to avoid race conditions:
// Wrong - race condition
go func() {
wg.Add(1) // might run after Wait()
defer wg.Done()
}()
// Correct
wg.Add(1)
go func() {
defer wg.Done()
}()Try it yourself
package main
import (
"fmt"
)
func main() {
// Read the number of downloads
var n int
fmt.Scanln(&n)
// Read download information
downloads := make([]Download, n)
for i := 0; i < n; i++ {
var id int
var filename string
var size int
fmt.Scanln(&id)
fmt.Scanln(&filename)
fmt.Scanln(&size)
downloads[i] = Download{
ID: id,
Filename: filename,
Size: size,
}
}
// TODO: Call StartDownloads with the downloads slice
// TODO: Print the summary: "All downloads complete: [total]KB total"
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool