Concurrency & Delay in Go
Deep dive into Go concurrency: goroutines, channels, WaitGroups, mutexes, select, context cancellation, timeouts, and real-world concurrent backend patterns.
Waiting & Goroutines
Use `time.Sleep` to pause the current goroutine without wasting CPU, and the `go` keyword to start a new goroutine that runs concurrently. Goroutines are lightweight threads managed by the Go runtime, so creating many of them is cheap compared to operating-system threads. In this lesson you will see how to delay work, how to start a goroutine, and why the program sometimes needs extra waiting so goroutines have time to finish before `main` exits.
1. Sleep for N milliseconds
Block the current goroutine for a duration.
2. Run a Function Concurrently
Start a new goroutine with go.
Coordinating Concurrent Work
Real programs rarely start just one goroutine: they often launch many goroutines that must be coordinated. You can wait for all goroutines to finish using `sync.WaitGroup`, or coordinate using channels that signal completion or send results. In this lesson you will learn patterns for waiting for every goroutine to complete and for reacting as soon as the first goroutine finishes.
1. Wait for All Goroutines — WaitGroup
Block until every goroutine has finished.
2. Wait for All Goroutines — Channel
Receive one signal per goroutine; after N receives, all are done.
3. Wait for First Goroutine — Channel
Proceed when the first goroutine sends a result.
Channels & Communication
Channels are typed pipes that connect concurrent parts of a Go program. One goroutine sends values into a channel and another goroutine receives those values, allowing you to safely share data without manual locking. In this lesson you will see how to create a channel, send and receive values, and use `range` to consume all values until the channel is closed.
1. Range over a Channel
Producer sends, consumer ranges until close.
Buffered vs Unbuffered Channels
Unbuffered channels block both send and receive until the other side is ready, which tightly synchronizes goroutines. Buffered channels add a finite queue in the middle so some sends can proceed immediately without a waiting receiver, up to the channel capacity. Understanding when operations block is critical to designing correct, deadlock-free concurrent programs.
1. Unbuffered Channel Blocking
Send waits for a receiver; receive waits for a sender.
2. Buffered Channel Capacity
Sends can proceed until the buffer is full.
3. Buffered Channel with Goroutine
Combine buffering with concurrent sends.
Mutex, RWMutex & Atomic Operations
When multiple goroutines read and write the same data, you must synchronize access or your program will have data races and undefined behavior. Go provides `sync.Mutex` for simple mutual exclusion, `sync.RWMutex` for many-readers / single-writer scenarios, and `sync/atomic` for fast, low-level operations on simple values like counters. This lesson shows when to choose each tool and how to apply it correctly.
1. Mutex for Exclusive Access
Only one goroutine can hold the lock at a time.
2. RWMutex for Many Readers, One Writer
Multiple readers can proceed together; writers get exclusive access.
3. Atomic Operations on Integers
Use sync/atomic for simple counters.
Worker Pools
A worker pool uses a fixed number of long-lived goroutines to process many incoming tasks from a jobs channel. Instead of creating a new goroutine for every task, which can overwhelm the scheduler or external resources, you bound concurrency by the size of the pool. This pattern is essential for rate-limiting work such as HTTP requests, database queries, or CPU-heavy computation.
1. Fixed-Size Worker Pool
Create N workers that read from a jobs channel and send results.
Goroutine Leaks
A goroutine leak happens when a goroutine is started but never finishes, usually because it is blocked forever on a channel operation, lock, or other wait. In small scripts this might go unnoticed, but in long-running services it slowly consumes memory and other resources. This lesson helps you recognize common leak patterns and design goroutines so they can always exit when their work is done or when the program is shutting down.
1. Example of a Goroutine Leak
Goroutine waits on a channel that is never closed or sent to.
2. Avoiding Leaks with Close or Cancellation
Design goroutines so they can exit when work is done or when cancelled.
Using the Go Race Detector
Data races are bugs that occur when two goroutines access the same memory at the same time and at least one of them writes, without proper synchronization. They can cause crashes and hard-to-reproduce behavior. The Go race detector instruments your program at build time and then watches for racy accesses at runtime; you enable it with the `-race` flag when running or testing code. In this lesson you will see how to trigger, detect, and fix a simple race.
1. Program with a Data Race
Two goroutines write the same variable without synchronization.
2. Fixing the Race with a Mutex
Synchronize access to shared data.
3. Running the Race Detector
Use -race with go run or go test.
Timeouts with select
`select` lets a goroutine wait on multiple channel operations at once and continue with whichever is ready first. By combining `select` with `time.After`, you can give an operation a maximum amount of time to complete and fall back to a timeout path if it is too slow. This pattern is the foundation for building responsive, resilient concurrent systems in Go.
1. time.After with select
Either receive a value or time out.