Sync & Context
Production Go code requires proper synchronization and cancellation.
The
sync package provides primitives for coordinating goroutines, while
the
context package enables cancellation, timeouts, and request-scoped
values.
Master these essential tools for building robust concurrent applications.
Figure: Sync Primitives (WaitGroup, Mutex, RWMutex, Once) and Context Package (Cancellation, Timeout, Values)
sync.WaitGroup
A WaitGroup waits for a collection of goroutines to finish. It's the
most
common way to coordinate multiple goroutines:
Click Run to execute your code
Add(n)- Increment the counter by nDone()- Decrement the counter by 1 (same as Add(-1))Wait()- Block until counter reaches 0
wg.Add() before
starting the
goroutine, and use defer wg.Done() to ensure Done() is called even
if the
goroutine panics.
sync.Mutex
A Mutex (mutual exclusion lock) protects shared data from concurrent
access:
Click Run to execute your code
Lock() and
Unlock() is the critical section. Only one goroutine can execute it
at a time.
defer mu.Unlock() immediately
after
mu.Lock() to ensure the lock is released even if the function
panics.
sync.RWMutex
A RWMutex is a reader/writer mutual exclusion lock. Multiple readers
can hold
the lock simultaneously, but writers have exclusive access:
Click Run to execute your code
| Lock Type | Methods | Use Case |
|---|---|---|
| Read Lock | RLock(), RUnlock() |
Multiple concurrent readers |
| Write Lock | Lock(), Unlock() |
Exclusive write access |
sync.Once
sync.Once ensures a function is executed only once, even when called
from
multiple goroutines. Perfect for initialization:
Click Run to execute your code
sync.Once is commonly used to
implement
thread-safe singletons in Go.
Context Package
The context package provides a way to carry deadlines, cancellation
signals,
and request-scoped values across API boundaries and between goroutines:
Context Types
// Root contexts
ctx := context.Background() // Main context, never cancelled
ctx := context.TODO() // Placeholder when context is unclear
// Derived contexts
ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithTimeout(parent, duration)
ctx, cancel := context.WithDeadline(parent, time)
ctx = context.WithValue(parent, key, value)
Context Cancellation
Use WithCancel to create a cancellable context:
Click Run to execute your code
cancel() when you're
done with a
context, even if it's not explicitly cancelled. Use defer cancel()
to ensure
cleanup.
Context Timeout & Deadline
Use WithTimeout or WithDeadline to automatically cancel
operations
after a duration:
Click Run to execute your code
Deadline Example
Click Run to execute your code
Context Values
Context can carry request-scoped values, but use this feature sparingly:
Click Run to execute your code
Common Mistakes
1. Forgetting to call Done()
// ❌ Wrong - goroutine might panic before Done()
go func() {
wg.Add(1)
// ... work ...
wg.Done() // Might not be called!
}()
// ✅ Correct - defer ensures Done() is called
go func() {
wg.Add(1)
defer wg.Done()
// ... work ...
}()
2. Not deferring Unlock()
// ❌ Wrong - might forget to unlock
func (c *Counter) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock() // Might be skipped if panic occurs
}
// ✅ Correct - defer ensures unlock
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
3. Not calling cancel()
// ❌ Wrong - context leak
func doWork() {
ctx, cancel := context.WithTimeout(parent, time.Second)
// ... work ...
// Forgot to call cancel()!
}
// ✅ Correct - always defer cancel
func doWork() {
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()
// ... work ...
}
Exercise: Concurrent Task Processor
Task: Build a concurrent task processor with cancellation support.
Requirements:
- Process 100 tasks using 5 workers
- Use WaitGroup to coordinate workers
- Use Context to support cancellation
- Use Mutex to safely collect results
- Cancel all workers after 3 seconds
Show Solution
package main
import (
"context"
"fmt"
"sync"
"time"
)
type Results struct {
mu sync.Mutex
results []int
}
func (r *Results) Add(value int) {
r.mu.Lock()
defer r.mu.Unlock()
r.results = append(r.results, value)
}
func worker(ctx context.Context, id int, tasks <-chan int, results *Results, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d cancelled\n", id)
return
case task, ok := <-tasks:
if !ok {
return
}
// Process task
result := task * 2
results.Add(result)
fmt.Printf("Worker %d processed task %d\n", id, task)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
tasks := make(chan int, 100)
results := &Results{}
var wg sync.WaitGroup
// Start workers
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(ctx, i, tasks, results, &wg)
}
// Send tasks
go func() {
for i := 1; i <= 100; i++ {
tasks <- i
}
close(tasks)
}()
// Wait for completion or timeout
wg.Wait()
fmt.Printf("\nProcessed %d tasks\n", len(results.results))
}
Summary
- sync.WaitGroup coordinates multiple goroutines
- sync.Mutex protects shared data with exclusive locks
- sync.RWMutex allows multiple readers or one writer
- sync.Once ensures initialization happens only once
- context.Context carries cancellation signals and deadlines
- WithCancel creates manually cancellable contexts
- WithTimeout/WithDeadline creates auto-cancelling contexts
- Context values should only be used for request-scoped data
- Always defer Done(), Unlock(), and cancel()
What's Next?
Now that you understand sync primitives and context, you're ready to explore Packages & Modules. In the next lesson, you'll learn how to organize your code into reusable packages and manage dependencies with Go modules.
Enjoying these tutorials?