Lesson 13 of 18
Go Concurrency Patterns
Master Go concurrency — goroutines, channels, select, WaitGroups, mutexes, and common patterns like fan-out/fan-in and worker pools.
What you'll learn
- ✓How goroutines and channels enable concurrency in Go
- ✓Select statements for multiplexing channels
- ✓Fan-out/fan-in, worker pools, and pipeline patterns
- ✓When to use channels vs mutexes
Prerequisites
- •Go basics (functions, structs, interfaces)
- •Understanding of concurrent vs parallel execution
Go was designed with concurrency as a first-class citizen. Goroutines are cheap, channels are typed, and the select statement lets you multiplex. This guide covers the patterns you will use in production.
Goroutines
A goroutine is a lightweight thread managed by the Go runtime. Start one with the go keyword.
func main() {
go sayHello("Alice")
go sayHello("Bob")
time.Sleep(time.Second) // wait for goroutines (don't do this in production)
}
func sayHello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
Goroutines cost about 2KB of stack (grows as needed) vs 1MB+ for OS threads. You can run millions of them.
Channels
Channels are typed conduits for communication between goroutines.
func main() {
ch := make(chan string)
go func() {
ch <- "hello from goroutine"
}()
msg := <-ch
fmt.Println(msg)
}
Buffered channels
Buffered channels allow sends without an immediate receiver, up to the buffer size.
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 would block — buffer full
Directional channels
Restrict channel direction in function signatures to prevent misuse.
func producer(out chan<- int) {
for i := 0; i < 5; i++ {
out <- i
}
close(out)
}
func consumer(in <-chan int) {
for val := range in {
fmt.Println(val)
}
}
Select
select waits on multiple channel operations. It is the concurrency equivalent of switch.
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "one"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "two"
}()
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
}
}
Timeout with select
select {
case result := <-ch:
fmt.Println("Got result:", result)
case <-time.After(3 * time.Second):
fmt.Println("Timed out")
}
WaitGroup
sync.WaitGroup waits for a collection of goroutines to finish.
func main() {
var wg sync.WaitGroup
urls := []string{
"https://example.com",
"https://example.org",
"https://example.net",
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
fmt.Println("Error:", err)
return
}
resp.Body.Close()
fmt.Printf("%s → %d\n", u, resp.StatusCode)
}(url)
}
wg.Wait()
fmt.Println("All requests complete")
}
Pattern: Fan-out / Fan-in
Fan-out: multiple goroutines read from the same channel. Fan-in: multiple channels feed into one.
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
Pattern: Worker pool
A fixed number of goroutines process jobs from a shared channel.
func workerPool(jobs <-chan int, results chan<- int, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
workerPool(jobs, results, 5)
for i := 0; i < 20; i++ {
jobs <- i
}
close(jobs)
for result := range results {
fmt.Println("Result:", result)
}
}
Pattern: Pipeline
Each stage is a goroutine that receives from one channel and sends to another.
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
ch := generate(2, 3, 4, 5)
result := square(ch)
for val := range result {
fmt.Println(val) // 4, 9, 16, 25
}
}
Context for cancellation
The context package provides cancellation, deadlines, and request-scoped values.
func longRunning(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// do work
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := longRunning(ctx); err != nil {
fmt.Println("Stopped:", err) // context deadline exceeded
}
}
Channels vs mutexes
| Use case | Channels | Mutexes |
|---|---|---|
| Passing data between goroutines | Yes | No |
| Protecting shared state | Possible but awkward | Yes |
| Signaling events | Yes | No |
| Simple counters/caches | Overkill | Yes |
The proverb: “Don’t communicate by sharing memory; share memory by communicating.” But use the right tool — a sync.Mutex protecting a map is simpler than a channel-based equivalent.
Summary
Go concurrency is built on goroutines, channels, and select. The patterns — fan-out/fan-in, worker pools, pipelines — compose from these primitives. Use sync.WaitGroup for simple coordination, context for cancellation, and reach for mutexes when protecting shared state is simpler than channel choreography.
Progress is saved locally to your browser.