🧠 Concurrency in Go: Goroutines, Channels, and Patterns
Go was designed with concurrency as a first-class citizen. Unlike many other languages that bolt on concurrency, Go’s model—centered around goroutines and channels—is simple, elegant, and incredibly powerful.
In this article, we’ll break down:
- What concurrency is in Go
- How goroutines and channels work
- Real-world concurrency patterns
- Code examples you can plug into your own projects
🚦 Concurrency vs. Parallelism
- Concurrency is about managing multiple tasks at once.
- Parallelism is about doing multiple tasks simultaneously.
Go lets you write concurrent code easily, and if your CPU allows, it can also run in parallel.
🌀 Goroutines
A goroutine is a lightweight thread managed by the Go runtime.
|
|
go sayHello() starts the function in the background.
⚠️ Without time.Sleep, the main function may exit before the goroutine finishes.
📡 Channels
Channels allow goroutines to communicate safely.
|
|
chan T
is a channel of type T<-ch
receivesch <-
sends
🔄 Buffered Channels
Buffered channels don’t block until full.
|
|
❌ Closing Channels
You can close a channel to indicate no more values will be sent.
|
|
🧱 Select Statement
select
lets you wait on multiple channel operations.
|
|
🛠️ Concurrency Patterns
Fan-Out / Fan-In
Fan-Out: Multiple goroutines read from the same channel.
Fan-In: Multiple goroutines send into a single channel.
1 2 3 4 5 6 7 8
func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("Worker %d processing job %d\n", id, j) time.Sleep(time.Second) results <- j * 2 } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
func main() { jobs := make(chan int, 5) results := make(chan int, 5) for w := 1; w <= 3; w++ { go worker(w, jobs, results) } for j := 1; j <= 5; j++ { jobs <- j } close(jobs) for a := 1; a <= 5; a++ { fmt.Println("Result:", <-results) } }
Worker Pool
You can create a pool of workers that handle jobs concurrently with limited resources.
✅ Use buffered channels and sync.WaitGroup
for coordination.
Timeout with select
1 2 3 4 5 6 7 8 9 10 11 12 13
c := make(chan string) go func() { time.Sleep(2 * time.Second) c <- "done" }() select { case res := <-c: fmt.Println(res) case <-time.After(1 * time.Second): fmt.Println("timeout") }
⚖️ sync.WaitGroup
Use it to wait for all goroutines to finish.
|
|
🧠 Final Thoughts
Go makes concurrency not only powerful—but approachable. You don’t need threads or semaphores to build safe, concurrent systems. ✅ Key Takeaways:
- Use goroutines for lightweight concurrency.
- Use channels for safe communication.
- Master select, worker pools, and timeouts for production-grade patterns.
🚀 Follow me on norbix.dev for more insights on Go, system design, and engineering wisdom.