Chapter 9: Concurrency in Go
In this chapter, you’ll learn how Go makes concurrency simple and efficient. We’ll explore goroutines, channels, and how they work together…
Chapter 9: Concurrency in Go
In this chapter, you’ll learn how Go makes concurrency simple and efficient. We’ll explore goroutines, channels, and how they work together to help your programs perform multiple tasks at the same time.

What You Will Learn
By the end of this chapter, you will understand:
- Why Go is designed for concurrency
- How goroutines work under the runtime scheduler
- Channels and communication patterns
selectfor coordinating concurrent work- Worker pools and practical concurrency patterns
- Common pitfalls: deadlocks & race conditions
- Using the
contextpackage to control cancellations and timeouts - Synchronization tools:
Mutex,WaitGroup, andCond
This is essential knowledge for writing scalable, production-grade Go software.
9.1 Why Go Is Good at Concurrency
Go was created at Google to solve a core problem:
Handling massive numbers of simultaneous network requests efficiently.
Traditional threads are heavy and expensive.
Go introduces goroutines: lightweight threads scheduled by the Go runtime.
| Feature | OS Thread | Goroutine |
| --------------- | --------- | --------------------- |
| Memory | ~1–2 MB | ~2 KB |
| Switch cost | Expensive | Very cheap |
| Count supported | Hundreds | Thousands to millions |
9.2 Goroutines
Goroutines work by running functions concurrently with the main program. To create a goroutine, you simply add the go keyword before a function call, which tells the Go runtime to run that function as a separate, lightweight thread of execution.
Syntax:
go doSomething()
Example:
package main
import (
"fmt"
"time"
)
func say(message string) {
fmt.Println(message)
}
func main() {
go say("Hello from goroutine!")
fmt.Println("Main exiting soon...")
time.Sleep(time.Second)
}
How They Work Internally
- Goroutines run on multiple OS threads
- A scheduler applies M:N threading: Many goroutines → few OS threads
- Blocking calls (I/O, waiting) don’t block all goroutines
9.3 Channels
Go channels act as typed pipes for safe communication between goroutines, allowing one goroutine to send a value to another. Channels allow goroutines to communicate safely.
Create a channel:
ch := make(chan int)
Send:
ch <- 10
Receive:
value := <-ch
Unbuffered Channels
Block until both send and receive happen.
ch := make(chan string)
go func() {
ch <- "hello"
}()
msg := <-ch
fmt.Println(msg)
Buffered Channels
Store messages without waiting (until buffer full).
ch := make(chan int, 2)
ch <- 1
ch <- 2
If you add a third send → code blocks.
9.4 Channel Patterns
✔ Closing Channels
close(ch)
Receivers can check:
value, ok := <-ch
if !ok {
fmt.Println("channel closed")
}
✔ Range Over Channel
for v := range ch {
fmt.Println(v)
}
Works until channel closes.
9.5 The select Statement
select waits on multiple channel operations.
select {
case msg := <-ch1:
fmt.Println("from ch1:", msg)
case msg := <-ch2:
fmt.Println("from ch2:", msg)
default:
fmt.Println("no message yet")
}
Useful for:
- Timeouts
- Handling multiple workers
- Non-blocking operations
9.6 Worker Pools (Real-World Pattern)
A worker pool processes jobs concurrently with limited workers.
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
for i := 1; i <= 3; i++ {
go worker(i, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for i := 1; i <= 5; i++ {
fmt.Println(<-results)
}
}
This pattern scales efficiently.
Deadlocks & Race Conditions
❌ Deadlock Example
ch := make(chan int)
ch <- 10 // nobody receives → deadlock
Avoid by ensuring:
- Receivers exist
- Channels close properly
- Use select default or buffers when appropriate
❌ Race Condition Example
count := 0
for i := 0; i < 1000; i++ {
go func() { count++ }()
}
Fix via mutex (covered below) or channel-based ownership.
9.7 Context Package (Modern Control Tool)
Used for:
- Canceling goroutines
- Timeouts
- Deadlines
- Request scoping
Example timeout:
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
select {
case <-time.After(2 * time.Second):
fmt.Println("work finished")
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err())
}
9.8 Synchronization Tools (sync package)
✔ WaitGroup
Wait for goroutines to finish.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("working...")
}()
wg.Wait()
✔ Mutex
Protect shared memory.
var mu sync.Mutex
count := 0
mu.Lock()
count++
mu.Unlock()
✔ Cond (Advanced)
Used for coordination when goroutines must wait for a condition.
cond := sync.NewCond(&sync.Mutex{})
Useful in low-level designs (locks, queues).
Summary of Chapter 9
You now understand:
✔ Why Go excels at concurrency
✔ How goroutines work and scale
✔ Channels for safe communication
✔ Buffered vs unbuffered behavior
✔ select for concurrent coordination
✔ Worker pools for real systems
✔ Deadlock and race detection
✔ context for cancellation and timeouts
✔ Synchronization primitives (WaitGroup, Mutex, Cond)
메타데이터
- post_id
- edb766decc7d
- slug
- chapter-9-concurrency-in-go-edb766decc7d
- url
- https://medium.com/@imadityarathore/chapter-9-concurrency-in-go-edb766decc7d
- canonical_url
- https://medium.com/@imadityarathore/chapter-9-concurrency-in-go-edb766decc7d
- author_url
- https://medium.com/@imadityarathore
- status
- ok
- fetched_at
- 2026-06-24 13:29:15