← Back to list

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…

Aditya Rathore · 2025-11-25 06:55 · 0 claps · 3.2 min read
#concurrency-in-golang #go-tutorial #golang
Open on Medium ↗

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
  • select for coordinating concurrent work
  • Worker pools and practical concurrency patterns
  • Common pitfalls: deadlocks & race conditions
  • Using the context package to control cancellations and timeouts
  • Synchronization tools: Mutex, WaitGroup, and Cond

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